feat: add review shortcut confirmation setting
This commit is contained in:
@@ -121,7 +121,9 @@ Fixed bindings:
|
|||||||
| `Tab` `Shift+Tab` / `←` `→` | Empty editor | Switch tabs without closing editor |
|
| `Tab` `Shift+Tab` / `←` `→` | Empty editor | Switch tabs without closing editor |
|
||||||
| Arrow keys / `Tab` | Non-empty editor | Stay in editor for cursor movement |
|
| Arrow keys / `Tab` | Non-empty editor | Stay in editor for cursor movement |
|
||||||
|
|
||||||
Config file: `~/.pi/.../extensions/eko24ive-pi-ask.json`
|
Review-tab shortcuts can optionally require the same number key twice via `behaviour.doublePressReviewShortcuts`.
|
||||||
|
|
||||||
|
Config file: `~/.pi/.../eko24ive-pi-ask.json`
|
||||||
|
|
||||||
You can edit this file yourself, ask pi to edit it for you, or use the read-only `Keymaps` tab to find the exact path and active bindings.
|
You can edit this file yourself, ask pi to edit it for you, or use the read-only `Keymaps` tab to find the exact path and active bindings.
|
||||||
|
|
||||||
@@ -129,7 +131,10 @@ You can edit this file yourself, ask pi to edit it for you, or use the read-only
|
|||||||
{
|
{
|
||||||
"schemaVersion": 1,
|
"schemaVersion": 1,
|
||||||
"behaviour": {
|
"behaviour": {
|
||||||
"autoSubmitWhenAnsweredWithoutNotes": false
|
"autoSubmitWhenAnsweredWithoutNotes": false,
|
||||||
|
"confirmDismissWhenDirty": true,
|
||||||
|
"doublePressReviewShortcuts": true,
|
||||||
|
"showFooterHints": true
|
||||||
},
|
},
|
||||||
"keymaps": {
|
"keymaps": {
|
||||||
"cancel": "esc",
|
"cancel": "esc",
|
||||||
@@ -148,7 +153,7 @@ Accepted notation follows pi-tui key ids. Common aliases are normalized, for exa
|
|||||||
|
|
||||||
After installation, the extension registers the `ask_user` tool and `/ask-settings` command.
|
After installation, the extension registers the `ask_user` tool and `/ask-settings` command.
|
||||||
|
|
||||||
Agents can auto-discover and call `ask_user` when they need clarification instead of guessing. In interactive sessions, it opens a terminal UI flow for structured answers, supports native pi-style `@` file references while typing answers or notes, and returns normalized answers back to the agent. The ask settings modal is available both from `?` in the ask flow and from the `/ask-settings` command, with `Keymaps` and `Behaviour` tabs. `Keymaps` is read-only and shows the active resolved bindings plus the config file path. Behaviour settings are saved explicitly with `Ctrl+S`.
|
Agents can auto-discover and call `ask_user` when they need clarification instead of guessing. In interactive sessions, it opens a terminal UI flow for structured answers, supports native pi-style `@` file references while typing answers or notes, and returns normalized answers back to the agent. The ask settings modal is available both from `?` in the ask flow and from the `/ask-settings` command, with `Keymaps` and `Behaviour` tabs. `Keymaps` is read-only and shows the active resolved bindings plus the config file path. Behaviour settings are saved explicitly with `Ctrl+S`, including whether review-tab number shortcuts require a double press.
|
||||||
|
|
||||||
You can also talk to pi to configure this extension. When asked to customize pi-ask settings or keymaps, the agent is instructed to read the bundled `docs/configuration.md` guide first and then edit the config file accordingly.
|
You can also talk to pi to configure this extension. When asked to customize pi-ask settings or keymaps, the agent is instructed to read the bundled `docs/configuration.md` guide first and then edit the config file accordingly.
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -202,7 +202,7 @@ This document defines the stable external behavior. It does not explain internal
|
|||||||
- transcript-friendly call and result rendering
|
- transcript-friendly call and result rendering
|
||||||
- ask settings modal with `Keymaps` and `Behaviour` tabs
|
- ask settings modal with `Keymaps` and `Behaviour` tabs
|
||||||
- `?` in the ask flow and `/ask-settings` in pi open the same ask settings modal
|
- `?` in the ask flow and `/ask-settings` in pi open the same ask settings modal
|
||||||
- `Behaviour` exposes persisted user settings for `Auto-submit when answered without notes`, `Confirm dismiss when dirty`, and `Show footer hints`
|
- `Behaviour` exposes persisted user settings for `Auto-submit when answered without notes`, `Confirm dismiss when dirty`, `Double-press review shortcuts`, and `Show footer hints`
|
||||||
- `Keymaps` is a persisted config section for `cancel`, `dismiss`, `toggle`, `confirm`, `optionNote`, and `questionNote`
|
- `Keymaps` is a persisted config section for `cancel`, `dismiss`, `toggle`, `confirm`, `optionNote`, and `questionNote`
|
||||||
- the `Keymaps` tab is read-only and shows the active bindings plus the config file path
|
- the `Keymaps` tab is read-only and shows the active bindings plus the config file path
|
||||||
- if the flow is already on the review tab, all questions are answered, and no notes exist, enabling auto-submit can complete the current ask flow immediately
|
- if the flow is already on the review tab, all questions are answered, and no notes exist, enabling auto-submit can complete the current ask flow immediately
|
||||||
@@ -217,6 +217,7 @@ Main flow:
|
|||||||
- `Tab`, `Shift+Tab`, `Left`, `Right`: move between tabs
|
- `Tab`, `Shift+Tab`, `Left`, `Right`: move between tabs
|
||||||
- `Up`, `Down`: move between options
|
- `Up`, `Down`: move between options
|
||||||
- `1..9`: select or toggle the matching option; on the review tab, `1`, `2`, and `3` trigger `Submit`, `Elaborate`, and `Cancel`
|
- `1..9`: select or toggle the matching option; on the review tab, `1`, `2`, and `3` trigger `Submit`, `Elaborate`, and `Cancel`
|
||||||
|
- when `Double-press review shortcuts` is enabled, review-tab `1`, `2`, and `3` require the same key twice without a timeout, and the review screen shows an inline hint for the pending action
|
||||||
- the following actions are configurable via persisted `keymaps`: `Enter` by default confirms or submits, `Esc` cancels, `Ctrl+C` dismisses, `Space` toggles, `n` edits the active option note, and `Shift+N` edits the current question note
|
- the following actions are configurable via persisted `keymaps`: `Enter` by default confirms or submits, `Esc` cancels, `Ctrl+C` dismisses, `Space` toggles, `n` edits the active option note, and `Shift+N` edits the current question note
|
||||||
|
|
||||||
Editing flow:
|
Editing flow:
|
||||||
@@ -238,7 +239,7 @@ If `ctx.hasUI === false`, the tool returns a `Needs user input` message in `cont
|
|||||||
|
|
||||||
Validation is handled inside the tool so malformed calls produce the same structured error shape as other invalid payloads instead of relying on pre-execution schema failures.
|
Validation is handled inside the tool so malformed calls produce the same structured error shape as other invalid payloads instead of relying on pre-execution schema failures.
|
||||||
|
|
||||||
The ask flow subscribes to runtime settings updates while open. In practice, this means changing `Auto-submit when answered without notes`, `Confirm dismiss when dirty`, `Show footer hints`, or reloading config-backed keymaps can affect the in-progress ask flow immediately instead of only future asks.
|
The ask flow subscribes to runtime settings updates while open. In practice, this means changing `Auto-submit when answered without notes`, `Confirm dismiss when dirty`, `Double-press review shortcuts`, `Show footer hints`, or reloading config-backed keymaps can affect the in-progress ask flow immediately instead of only future asks.
|
||||||
|
|
||||||
The fallback message includes normalized pending questions and options so the caller can re-ask them manually. `details.questions` still contains normalized question metadata, while `details.answers` stays empty until a user responds.
|
The fallback message includes normalized pending questions and options so the caller can re-ask them manually. `details.questions` still contains normalized question metadata, while `details.answers` stays empty until a user responds.
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import type { AskConfig, AskConfigFileV1 } from "./schema.ts";
|
|||||||
export const DEFAULT_ASK_CONFIG: AskConfig = {
|
export const DEFAULT_ASK_CONFIG: AskConfig = {
|
||||||
behaviour: {
|
behaviour: {
|
||||||
autoSubmitWhenAnsweredWithoutNotes: false,
|
autoSubmitWhenAnsweredWithoutNotes: false,
|
||||||
confirmDismissWhenDirty: false,
|
confirmDismissWhenDirty: true,
|
||||||
|
doublePressReviewShortcuts: true,
|
||||||
showFooterHints: true,
|
showFooterHints: true,
|
||||||
},
|
},
|
||||||
keymaps: {
|
keymaps: {
|
||||||
@@ -23,6 +24,9 @@ export function normalizeAskConfig(
|
|||||||
confirmDismissWhenDirty:
|
confirmDismissWhenDirty:
|
||||||
config?.behaviour?.confirmDismissWhenDirty ??
|
config?.behaviour?.confirmDismissWhenDirty ??
|
||||||
DEFAULT_ASK_CONFIG.behaviour.confirmDismissWhenDirty,
|
DEFAULT_ASK_CONFIG.behaviour.confirmDismissWhenDirty,
|
||||||
|
doublePressReviewShortcuts:
|
||||||
|
config?.behaviour?.doublePressReviewShortcuts ??
|
||||||
|
DEFAULT_ASK_CONFIG.behaviour.doublePressReviewShortcuts,
|
||||||
showFooterHints:
|
showFooterHints:
|
||||||
config?.behaviour?.showFooterHints ??
|
config?.behaviour?.showFooterHints ??
|
||||||
DEFAULT_ASK_CONFIG.behaviour.showFooterHints,
|
DEFAULT_ASK_CONFIG.behaviour.showFooterHints,
|
||||||
@@ -42,6 +46,8 @@ export function toAskConfigFileV1(config: AskConfig): AskConfigFileV1 {
|
|||||||
autoSubmitWhenAnsweredWithoutNotes:
|
autoSubmitWhenAnsweredWithoutNotes:
|
||||||
normalized.behaviour.autoSubmitWhenAnsweredWithoutNotes,
|
normalized.behaviour.autoSubmitWhenAnsweredWithoutNotes,
|
||||||
confirmDismissWhenDirty: normalized.behaviour.confirmDismissWhenDirty,
|
confirmDismissWhenDirty: normalized.behaviour.confirmDismissWhenDirty,
|
||||||
|
doublePressReviewShortcuts:
|
||||||
|
normalized.behaviour.doublePressReviewShortcuts,
|
||||||
showFooterHints: normalized.behaviour.showFooterHints,
|
showFooterHints: normalized.behaviour.showFooterHints,
|
||||||
},
|
},
|
||||||
keymaps: {
|
keymaps: {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export const AskConfigFileV1Schema = Type.Object({
|
|||||||
Type.Object({
|
Type.Object({
|
||||||
autoSubmitWhenAnsweredWithoutNotes: Type.Optional(Type.Boolean()),
|
autoSubmitWhenAnsweredWithoutNotes: Type.Optional(Type.Boolean()),
|
||||||
confirmDismissWhenDirty: Type.Optional(Type.Boolean()),
|
confirmDismissWhenDirty: Type.Optional(Type.Boolean()),
|
||||||
|
doublePressReviewShortcuts: Type.Optional(Type.Boolean()),
|
||||||
showFooterHints: Type.Optional(Type.Boolean()),
|
showFooterHints: Type.Optional(Type.Boolean()),
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
@@ -37,6 +38,7 @@ export interface AskConfig {
|
|||||||
behaviour: {
|
behaviour: {
|
||||||
autoSubmitWhenAnsweredWithoutNotes: boolean;
|
autoSubmitWhenAnsweredWithoutNotes: boolean;
|
||||||
confirmDismissWhenDirty: boolean;
|
confirmDismissWhenDirty: boolean;
|
||||||
|
doublePressReviewShortcuts: boolean;
|
||||||
showFooterHints: boolean;
|
showFooterHints: boolean;
|
||||||
};
|
};
|
||||||
keymaps: AskConfigKeymaps;
|
keymaps: AskConfigKeymaps;
|
||||||
|
|||||||
@@ -38,6 +38,10 @@ import {
|
|||||||
import type { AskInputCommand } from "./input.ts";
|
import type { AskInputCommand } from "./input.ts";
|
||||||
import { getInputCommand } from "./input.ts";
|
import { getInputCommand } from "./input.ts";
|
||||||
import { renderAskScreen } from "./render.ts";
|
import { renderAskScreen } from "./render.ts";
|
||||||
|
import {
|
||||||
|
getReviewShortcutHint,
|
||||||
|
resolveReviewShortcutDoublePress,
|
||||||
|
} from "./review-shortcuts.ts";
|
||||||
import { showAskSettingsModal } from "./show-settings-modal.ts";
|
import { showAskSettingsModal } from "./show-settings-modal.ts";
|
||||||
|
|
||||||
type CustomCallback = Parameters<ExtensionContext["ui"]["custom"]>[0];
|
type CustomCallback = Parameters<ExtensionContext["ui"]["custom"]>[0];
|
||||||
@@ -57,6 +61,7 @@ interface AskFlowController {
|
|||||||
dismissNotice?: string;
|
dismissNotice?: string;
|
||||||
done: Done;
|
done: Done;
|
||||||
editor: Editor;
|
editor: Editor;
|
||||||
|
pendingReviewShortcutActionIndex?: number;
|
||||||
settingsOpen: boolean;
|
settingsOpen: boolean;
|
||||||
state: AskState;
|
state: AskState;
|
||||||
suppressAutoInputForSelection: boolean;
|
suppressAutoInputForSelection: boolean;
|
||||||
@@ -94,6 +99,7 @@ function createAskFlowController(
|
|||||||
settingsOpen: false,
|
settingsOpen: false,
|
||||||
state: createInitialState(params),
|
state: createInitialState(params),
|
||||||
suppressAutoInputForSelection: false,
|
suppressAutoInputForSelection: false,
|
||||||
|
pendingReviewShortcutActionIndex: undefined,
|
||||||
theme,
|
theme,
|
||||||
tui,
|
tui,
|
||||||
unsubscribeConfig: () => {
|
unsubscribeConfig: () => {
|
||||||
@@ -136,6 +142,7 @@ function renderController(
|
|||||||
config: controller.config,
|
config: controller.config,
|
||||||
editor: controller.editor,
|
editor: controller.editor,
|
||||||
footerNotice: getDismissNotice(controller),
|
footerNotice: getDismissNotice(controller),
|
||||||
|
reviewShortcutHint: getActiveReviewShortcutHint(controller),
|
||||||
state: controller.state,
|
state: controller.state,
|
||||||
theme: controller.theme,
|
theme: controller.theme,
|
||||||
width,
|
width,
|
||||||
@@ -193,12 +200,15 @@ function handleNavigationCommand(
|
|||||||
) {
|
) {
|
||||||
switch (command.kind) {
|
switch (command.kind) {
|
||||||
case "moveTab":
|
case "moveTab":
|
||||||
|
clearReviewShortcutPending(controller);
|
||||||
commitState(controller, moveTab(controller.state, command.delta));
|
commitState(controller, moveTab(controller.state, command.delta));
|
||||||
return;
|
return;
|
||||||
case "moveOption":
|
case "moveOption":
|
||||||
|
clearReviewShortcutPending(controller);
|
||||||
commitState(controller, moveOption(controller.state, command.delta));
|
commitState(controller, moveOption(controller.state, command.delta));
|
||||||
return;
|
return;
|
||||||
case "toggleMulti":
|
case "toggleMulti":
|
||||||
|
clearReviewShortcutPending(controller);
|
||||||
handleToggleCurrentOption(controller);
|
handleToggleCurrentOption(controller);
|
||||||
return;
|
return;
|
||||||
case "openQuestionNote":
|
case "openQuestionNote":
|
||||||
@@ -208,20 +218,27 @@ function handleNavigationCommand(
|
|||||||
openOptionNote(controller);
|
openOptionNote(controller);
|
||||||
return;
|
return;
|
||||||
case "confirm":
|
case "confirm":
|
||||||
|
clearReviewShortcutPending(controller);
|
||||||
commitState(controller, confirmCurrentSelection(controller.state), {
|
commitState(controller, confirmCurrentSelection(controller.state), {
|
||||||
finish: true,
|
finish: true,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
case "cancel":
|
case "cancel":
|
||||||
|
clearReviewShortcutPending(controller);
|
||||||
handleExitFlow(controller, cancelFlow(controller.state));
|
handleExitFlow(controller, cancelFlow(controller.state));
|
||||||
return;
|
return;
|
||||||
case "numberShortcut":
|
case "numberShortcut":
|
||||||
|
if (handleReviewShortcutNumber(controller, command.digit)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
clearReviewShortcutPending(controller);
|
||||||
commitState(
|
commitState(
|
||||||
controller,
|
controller,
|
||||||
applyNumberShortcut(controller.state, command.digit)
|
applyNumberShortcut(controller.state, command.digit)
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
case "dismiss":
|
case "dismiss":
|
||||||
|
clearReviewShortcutPending(controller);
|
||||||
handleExitFlow(controller, dismissFlow(controller.state));
|
handleExitFlow(controller, dismissFlow(controller.state));
|
||||||
return;
|
return;
|
||||||
case "showSettings":
|
case "showSettings":
|
||||||
@@ -354,6 +371,56 @@ function clearDismissNotice(controller: AskFlowController) {
|
|||||||
controller.dismissNotice = undefined;
|
controller.dismissNotice = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearReviewShortcutPending(controller: AskFlowController) {
|
||||||
|
controller.pendingReviewShortcutActionIndex = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getActiveReviewShortcutHint(
|
||||||
|
controller: AskFlowController
|
||||||
|
): string | undefined {
|
||||||
|
if (
|
||||||
|
!(
|
||||||
|
isSubmitTab(controller.state) &&
|
||||||
|
controller.config.behaviour.doublePressReviewShortcuts
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return getReviewShortcutHint(controller.pendingReviewShortcutActionIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleReviewShortcutNumber(
|
||||||
|
controller: AskFlowController,
|
||||||
|
digit: number
|
||||||
|
): boolean {
|
||||||
|
if (
|
||||||
|
!(
|
||||||
|
isSubmitTab(controller.state) &&
|
||||||
|
controller.config.behaviour.doublePressReviewShortcuts
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolution = resolveReviewShortcutDoublePress(
|
||||||
|
digit,
|
||||||
|
controller.pendingReviewShortcutActionIndex
|
||||||
|
);
|
||||||
|
if (resolution.actionIndex === undefined) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
controller.pendingReviewShortcutActionIndex = resolution.pendingActionIndex;
|
||||||
|
const nextState = resolution.confirmed
|
||||||
|
? applyNumberShortcut(controller.state, digit)
|
||||||
|
: {
|
||||||
|
...controller.state,
|
||||||
|
activeSubmitActionIndex: resolution.actionIndex,
|
||||||
|
};
|
||||||
|
commitState(controller, nextState, { finish: resolution.confirmed });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
function getDismissNotice(controller: AskFlowController): string | undefined {
|
function getDismissNotice(controller: AskFlowController): string | undefined {
|
||||||
return controller.dismissNotice;
|
return controller.dismissNotice;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,13 +71,13 @@ function renderTabs(state: AskState, theme: Theme, width: number): string {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const submitText = " ✔ Submit ";
|
const reviewText = " ☰ Review ";
|
||||||
tabs.push({
|
tabs.push({
|
||||||
text: submitText,
|
text: reviewText,
|
||||||
width: visibleWidth(submitText),
|
width: visibleWidth(reviewText),
|
||||||
render: isSubmitTab(state)
|
render: isSubmitTab(state)
|
||||||
? theme.bg("selectedBg", theme.fg("text", submitText))
|
? theme.bg("selectedBg", theme.fg("text", reviewText))
|
||||||
: theme.fg("success", submitText),
|
: theme.fg("success", reviewText),
|
||||||
});
|
});
|
||||||
|
|
||||||
const activeIndex = Math.min(state.activeTabIndex, tabs.length - 1);
|
const activeIndex = Math.min(state.activeTabIndex, tabs.length - 1);
|
||||||
|
|||||||
+17
-1
@@ -13,7 +13,8 @@ export function renderSubmitScreen(
|
|||||||
lines: string[],
|
lines: string[],
|
||||||
state: AskState,
|
state: AskState,
|
||||||
theme: Theme,
|
theme: Theme,
|
||||||
width: number
|
width: number,
|
||||||
|
reviewShortcutHint?: string
|
||||||
) {
|
) {
|
||||||
const model = buildReviewScreenModel(state, width);
|
const model = buildReviewScreenModel(state, width);
|
||||||
if (model.layout === "wide") {
|
if (model.layout === "wide") {
|
||||||
@@ -32,6 +33,7 @@ export function renderSubmitScreen(
|
|||||||
)) {
|
)) {
|
||||||
lines.push(line);
|
lines.push(line);
|
||||||
}
|
}
|
||||||
|
appendReviewShortcutHint(lines, reviewShortcutHint, theme, width);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,6 +42,7 @@ export function renderSubmitScreen(
|
|||||||
lines.push(...reviewLines);
|
lines.push(...reviewLines);
|
||||||
lines.push("");
|
lines.push("");
|
||||||
lines.push(...actionLines);
|
lines.push(...actionLines);
|
||||||
|
appendReviewShortcutHint(lines, reviewShortcutHint, theme, width);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderSubmitReviewLines(
|
function renderSubmitReviewLines(
|
||||||
@@ -150,3 +153,16 @@ function renderSubmitActions(
|
|||||||
}
|
}
|
||||||
return lines;
|
return lines;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function appendReviewShortcutHint(
|
||||||
|
lines: string[],
|
||||||
|
hint: string | undefined,
|
||||||
|
theme: Theme,
|
||||||
|
width: number
|
||||||
|
) {
|
||||||
|
if (!hint) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lines.push("");
|
||||||
|
pushWrappedText(lines, hint, width, theme, "dim", " ", " ");
|
||||||
|
}
|
||||||
|
|||||||
+11
-2
@@ -13,12 +13,21 @@ import type { QuestionRenderContext, Theme } from "./render-types.ts";
|
|||||||
export function renderAskScreen(args: {
|
export function renderAskScreen(args: {
|
||||||
config: AskConfig;
|
config: AskConfig;
|
||||||
footerNotice?: string;
|
footerNotice?: string;
|
||||||
|
reviewShortcutHint?: string;
|
||||||
state: AskState;
|
state: AskState;
|
||||||
theme: Theme;
|
theme: Theme;
|
||||||
width: number;
|
width: number;
|
||||||
editor: QuestionRenderContext["editor"];
|
editor: QuestionRenderContext["editor"];
|
||||||
}): string[] {
|
}): string[] {
|
||||||
const { config, footerNotice, state, theme, width, editor } = args;
|
const {
|
||||||
|
config,
|
||||||
|
footerNotice,
|
||||||
|
reviewShortcutHint,
|
||||||
|
state,
|
||||||
|
theme,
|
||||||
|
width,
|
||||||
|
editor,
|
||||||
|
} = args;
|
||||||
const lines: string[] = [];
|
const lines: string[] = [];
|
||||||
const question = getCurrentQuestion(state);
|
const question = getCurrentQuestion(state);
|
||||||
const options = getRenderableOptions(question);
|
const options = getRenderableOptions(question);
|
||||||
@@ -26,7 +35,7 @@ export function renderAskScreen(args: {
|
|||||||
renderFrameHeader({ lines, state, theme, width });
|
renderFrameHeader({ lines, state, theme, width });
|
||||||
|
|
||||||
if (isSubmitTab(state)) {
|
if (isSubmitTab(state)) {
|
||||||
renderSubmitScreen(lines, state, theme, width);
|
renderSubmitScreen(lines, state, theme, width, reviewShortcutHint);
|
||||||
} else if (question) {
|
} else if (question) {
|
||||||
renderQuestionScreen({
|
renderQuestionScreen({
|
||||||
lines,
|
lines,
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { SUBMIT_CHOICES } from "../constants/text.ts";
|
||||||
|
|
||||||
|
const REVIEW_SHORTCUT_DIGIT_MIN = 1;
|
||||||
|
const REVIEW_SHORTCUT_DIGIT_MAX = 3;
|
||||||
|
|
||||||
|
export interface ReviewShortcutResolution {
|
||||||
|
actionIndex?: number;
|
||||||
|
confirmed: boolean;
|
||||||
|
pendingActionIndex?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveReviewShortcutDoublePress(
|
||||||
|
digit: number,
|
||||||
|
pendingActionIndex?: number
|
||||||
|
): ReviewShortcutResolution {
|
||||||
|
if (digit < REVIEW_SHORTCUT_DIGIT_MIN || digit > REVIEW_SHORTCUT_DIGIT_MAX) {
|
||||||
|
return { confirmed: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
const actionIndex = digit - 1;
|
||||||
|
if (pendingActionIndex === actionIndex) {
|
||||||
|
return { actionIndex, confirmed: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
actionIndex,
|
||||||
|
confirmed: false,
|
||||||
|
pendingActionIndex: actionIndex,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getReviewShortcutHint(pendingActionIndex?: number): string {
|
||||||
|
if (pendingActionIndex === undefined) {
|
||||||
|
return "Press 1, 2, or 3 twice to confirm a review action.";
|
||||||
|
}
|
||||||
|
|
||||||
|
const actionLabel = SUBMIT_CHOICES[pendingActionIndex];
|
||||||
|
if (!actionLabel) {
|
||||||
|
return "Press 1, 2, or 3 twice to confirm a review action.";
|
||||||
|
}
|
||||||
|
|
||||||
|
return `Press ${pendingActionIndex + 1} again to ${actionLabel}.`;
|
||||||
|
}
|
||||||
+122
-21
@@ -43,6 +43,7 @@ interface AskSettingsModalOptions {
|
|||||||
|
|
||||||
const MIN_WIDTH = 24;
|
const MIN_WIDTH = 24;
|
||||||
const COMPACT_WIDTH = 46;
|
const COMPACT_WIDTH = 46;
|
||||||
|
const COMPACT_DESCRIPTION_LINES = 2;
|
||||||
const TAB_GAP = " ";
|
const TAB_GAP = " ";
|
||||||
const DISCARD_WINDOW_MS = 1000;
|
const DISCARD_WINDOW_MS = 1000;
|
||||||
const SETTINGS_MODAL_KEYS = {
|
const SETTINGS_MODAL_KEYS = {
|
||||||
@@ -73,6 +74,15 @@ const BEHAVIOUR_ROWS = [
|
|||||||
},
|
},
|
||||||
label: "Confirm dismiss when dirty",
|
label: "Confirm dismiss when dirty",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
description:
|
||||||
|
"Require pressing 1, 2, or 3 twice on the review tab before triggering Submit, Elaborate, or Cancel.",
|
||||||
|
key: "doublePressReviewShortcuts",
|
||||||
|
getValue(config: AskConfig) {
|
||||||
|
return config.behaviour.doublePressReviewShortcuts;
|
||||||
|
},
|
||||||
|
label: "Double-press review shortcuts",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
description: "Show footer keymap hints at the bottom of the ask flow.",
|
description: "Show footer keymap hints at the bottom of the ask flow.",
|
||||||
key: "showFooterHints",
|
key: "showFooterHints",
|
||||||
@@ -305,8 +315,9 @@ function renderBehaviourTab(
|
|||||||
state: AskSettingsState,
|
state: AskSettingsState,
|
||||||
layout: SettingsLayout
|
layout: SettingsLayout
|
||||||
): string[] {
|
): string[] {
|
||||||
|
const compact = layout.innerW < COMPACT_WIDTH;
|
||||||
return BEHAVIOUR_ROWS.flatMap((row, index) =>
|
return BEHAVIOUR_ROWS.flatMap((row, index) =>
|
||||||
renderBehaviourRow(theme, state, layout, row, index)
|
renderBehaviourRow(theme, state, layout, row, index, compact)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,7 +326,8 @@ function renderBehaviourRow(
|
|||||||
state: AskSettingsState,
|
state: AskSettingsState,
|
||||||
layout: SettingsLayout,
|
layout: SettingsLayout,
|
||||||
row: (typeof BEHAVIOUR_ROWS)[number],
|
row: (typeof BEHAVIOUR_ROWS)[number],
|
||||||
index: number
|
index: number,
|
||||||
|
compact: boolean
|
||||||
): string[] {
|
): string[] {
|
||||||
const focused = index === state.behaviourFocusIndex;
|
const focused = index === state.behaviourFocusIndex;
|
||||||
const dirty =
|
const dirty =
|
||||||
@@ -334,15 +346,23 @@ function renderBehaviourRow(
|
|||||||
label = theme.bg("selectedBg", label);
|
label = theme.bg("selectedBg", label);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const descriptionLines = wrapText(row.description, layout.innerW - 4);
|
||||||
const lines = [
|
const lines = [
|
||||||
layout.line(truncateToWidth(label, layout.innerW - 1, "…")),
|
layout.line(truncateToWidth(label, layout.innerW - 1, "…")),
|
||||||
...wrapText(row.description, layout.innerW - 4).map((descLine) =>
|
...descriptionLines
|
||||||
layout.line(` ${theme.fg("dim", descLine)}`)
|
.slice(0, compact ? COMPACT_DESCRIPTION_LINES : descriptionLines.length)
|
||||||
),
|
.map((descLine) => layout.line(` ${theme.fg("dim", descLine)}`)),
|
||||||
];
|
];
|
||||||
if (index < BEHAVIOUR_ROWS.length - 1) {
|
if (!compact && index < BEHAVIOUR_ROWS.length - 1) {
|
||||||
lines.push(layout.line());
|
lines.push(layout.line());
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
compact &&
|
||||||
|
descriptionLines.length > COMPACT_DESCRIPTION_LINES &&
|
||||||
|
index < BEHAVIOUR_ROWS.length - 1
|
||||||
|
) {
|
||||||
|
lines.push(layout.line(` ${theme.fg("dim", "…")}`));
|
||||||
|
}
|
||||||
return lines;
|
return lines;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,7 +376,38 @@ function renderKeymapsTab(
|
|||||||
const lines: string[] = [];
|
const lines: string[] = [];
|
||||||
const compact = layout.innerW < COMPACT_WIDTH;
|
const compact = layout.innerW < COMPACT_WIDTH;
|
||||||
const { customizable, fixed } = getAskKeymaps(config);
|
const { customizable, fixed } = getAskKeymaps(config);
|
||||||
const headerLines = [
|
|
||||||
|
pushKeymapHeaderLines(lines, theme, layout, configPath, compact);
|
||||||
|
pushInvalidKeymapNotice(lines, theme, layout, notice, compact);
|
||||||
|
lines.push(layout.line());
|
||||||
|
if (compact) {
|
||||||
|
lines.push(
|
||||||
|
...renderCompactKeymapSection(theme, layout, "Customizable", customizable)
|
||||||
|
);
|
||||||
|
lines.push(layout.line());
|
||||||
|
lines.push(...renderCompactKeymapSection(theme, layout, "Fixed", fixed));
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
lines.push(layout.line(theme.fg("muted", "Customizable")));
|
||||||
|
pushKeymapRows(lines, theme, layout, customizable, compact);
|
||||||
|
lines.push(
|
||||||
|
layout.line(theme.fg("muted", "─".repeat(Math.max(1, layout.innerW - 2))))
|
||||||
|
);
|
||||||
|
lines.push(layout.line(theme.fg("muted", "Fixed")));
|
||||||
|
pushKeymapRows(lines, theme, layout, fixed, compact);
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pushKeymapHeaderLines(
|
||||||
|
lines: string[],
|
||||||
|
theme: Theme,
|
||||||
|
layout: SettingsLayout,
|
||||||
|
configPath: string,
|
||||||
|
compact: boolean
|
||||||
|
) {
|
||||||
|
const headerLines = compact
|
||||||
|
? [configPath, "Edit keymaps there, then /reload."]
|
||||||
|
: [
|
||||||
"Edit this config file to change customizable ask keymaps:",
|
"Edit this config file to change customizable ask keymaps:",
|
||||||
configPath,
|
configPath,
|
||||||
"Restart pi or run /reload after editing.",
|
"Restart pi or run /reload after editing.",
|
||||||
@@ -369,26 +420,76 @@ function renderKeymapsTab(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (notice?.kind === "error") {
|
}
|
||||||
lines.push(layout.line());
|
|
||||||
for (const line of wrapText(
|
function pushInvalidKeymapNotice(
|
||||||
"Using default keymaps this session because configured keymaps were rejected.",
|
lines: string[],
|
||||||
layout.innerW - 2
|
theme: Theme,
|
||||||
)) {
|
layout: SettingsLayout,
|
||||||
|
notice: AskConfigNotice | undefined,
|
||||||
|
compact: boolean
|
||||||
|
) {
|
||||||
|
if (notice?.kind !== "error") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const warningText = compact
|
||||||
|
? "Invalid config; using default keymaps this session."
|
||||||
|
: "Using default keymaps this session because configured keymaps were rejected.";
|
||||||
|
for (const line of wrapText(warningText, layout.innerW - 2)) {
|
||||||
lines.push(layout.line(theme.fg("warning", line)));
|
lines.push(layout.line(theme.fg("warning", line)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lines.push(layout.line());
|
|
||||||
lines.push(layout.line(theme.fg("muted", "Customizable")));
|
function renderCompactKeymapSection(
|
||||||
pushKeymapRows(lines, theme, layout, customizable, compact);
|
theme: Theme,
|
||||||
lines.push(
|
layout: SettingsLayout,
|
||||||
layout.line(theme.fg("muted", "─".repeat(Math.max(1, layout.innerW - 2))))
|
title: string,
|
||||||
);
|
keymaps: readonly AskKeyBinding[]
|
||||||
lines.push(layout.line(theme.fg("muted", "Fixed")));
|
): string[] {
|
||||||
pushKeymapRows(lines, theme, layout, fixed, compact);
|
const summary = keymaps
|
||||||
|
.map((hint) => `${hint.label} ${compactKeymapDescription(hint)}`)
|
||||||
|
.join(" · ");
|
||||||
|
const lines = [layout.line(theme.fg("muted", title))];
|
||||||
|
for (const line of wrapText(summary, layout.innerW - 2)) {
|
||||||
|
lines.push(layout.line(line));
|
||||||
|
}
|
||||||
return lines;
|
return lines;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function compactKeymapDescription(hint: AskKeyBinding): string {
|
||||||
|
switch (hint.id) {
|
||||||
|
case "cancel":
|
||||||
|
return "cancel";
|
||||||
|
case "dismiss":
|
||||||
|
return "dismiss";
|
||||||
|
case "toggle":
|
||||||
|
return "toggle";
|
||||||
|
case "confirm":
|
||||||
|
return "confirm";
|
||||||
|
case "optionNote":
|
||||||
|
return "option note";
|
||||||
|
case "questionNote":
|
||||||
|
return "question note";
|
||||||
|
case "settings":
|
||||||
|
return "settings";
|
||||||
|
case "nextTab":
|
||||||
|
return "next tab";
|
||||||
|
case "previousTab":
|
||||||
|
return "prev tab";
|
||||||
|
case "previousOption":
|
||||||
|
return "move";
|
||||||
|
case "nextOption":
|
||||||
|
return "next";
|
||||||
|
case "numberShortcut":
|
||||||
|
return "quick select";
|
||||||
|
case "fileReference":
|
||||||
|
return "file refs";
|
||||||
|
default:
|
||||||
|
return hint.description.toLowerCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function pushKeymapRows(
|
function pushKeymapRows(
|
||||||
lines: string[],
|
lines: string[],
|
||||||
theme: Theme,
|
theme: Theme,
|
||||||
|
|||||||
@@ -153,6 +153,8 @@ function deepEqual(left: AskConfig, right: AskConfig): boolean {
|
|||||||
right.behaviour.autoSubmitWhenAnsweredWithoutNotes &&
|
right.behaviour.autoSubmitWhenAnsweredWithoutNotes &&
|
||||||
left.behaviour.confirmDismissWhenDirty ===
|
left.behaviour.confirmDismissWhenDirty ===
|
||||||
right.behaviour.confirmDismissWhenDirty &&
|
right.behaviour.confirmDismissWhenDirty &&
|
||||||
|
left.behaviour.doublePressReviewShortcuts ===
|
||||||
|
right.behaviour.doublePressReviewShortcuts &&
|
||||||
left.behaviour.showFooterHints === right.behaviour.showFooterHints &&
|
left.behaviour.showFooterHints === right.behaviour.showFooterHints &&
|
||||||
left.keymaps.cancel === right.keymaps.cancel &&
|
left.keymaps.cancel === right.keymaps.cancel &&
|
||||||
left.keymaps.dismiss === right.keymaps.dismiss &&
|
left.keymaps.dismiss === right.keymaps.dismiss &&
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ test("dirty flow state is true for saved answers or editor drafts", () => {
|
|||||||
assert.equal(hasDirtyFlowState(noteEditing, "draft note"), true);
|
assert.equal(hasDirtyFlowState(noteEditing, "draft note"), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("confirm dismiss when dirty is opt-in and respects dirty state", () => {
|
test("confirm dismiss when dirty respects dirty state and config", () => {
|
||||||
const state = applyNumberShortcut(
|
const state = applyNumberShortcut(
|
||||||
createInitialState({
|
createInitialState({
|
||||||
questions: [
|
questions: [
|
||||||
@@ -48,20 +48,20 @@ test("confirm dismiss when dirty is opt-in and respects dirty state", () => {
|
|||||||
}),
|
}),
|
||||||
1
|
1
|
||||||
);
|
);
|
||||||
const enabledConfig = {
|
const disabledConfig = {
|
||||||
...DEFAULT_ASK_CONFIG,
|
...DEFAULT_ASK_CONFIG,
|
||||||
behaviour: {
|
behaviour: {
|
||||||
...DEFAULT_ASK_CONFIG.behaviour,
|
...DEFAULT_ASK_CONFIG.behaviour,
|
||||||
confirmDismissWhenDirty: true,
|
confirmDismissWhenDirty: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
assert.equal(
|
assert.equal(
|
||||||
shouldConfirmDirtyDismiss({ config: DEFAULT_ASK_CONFIG, state }),
|
shouldConfirmDirtyDismiss({ config: disabledConfig, state }),
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
shouldConfirmDirtyDismiss({ config: enabledConfig, state }),
|
shouldConfirmDirtyDismiss({ config: DEFAULT_ASK_CONFIG, state }),
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ test("wide header keeps all tabs and framing arrows on the tab row", () => {
|
|||||||
editor: mockEditor(),
|
editor: mockEditor(),
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.equal(lines[3], " ← ☐ One ☐ Two ✔ Submit →");
|
assert.equal(lines[3], " ← ☐ One ☐ Two ☰ Review →");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("narrow tab strip keeps active middle tab visible", () => {
|
test("narrow tab strip keeps active middle tab visible", () => {
|
||||||
@@ -143,7 +143,7 @@ test("narrow tab strip keeps submit tab visible when active", () => {
|
|||||||
editor: mockEditor(),
|
editor: mockEditor(),
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.equal(lines[3], " ← ✔ Submit →");
|
assert.equal(lines[3], " ← ☰ Review →");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("tab strip avoids truncation at narrow boundary widths", () => {
|
test("tab strip avoids truncation at narrow boundary widths", () => {
|
||||||
|
|||||||
@@ -111,6 +111,34 @@ test("submit screen stacks review above actions on narrow screens", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("submit screen can show a review shortcut hint below the actions", () => {
|
||||||
|
const state = createInitialState({
|
||||||
|
questions: [
|
||||||
|
{
|
||||||
|
id: "q1",
|
||||||
|
label: "Color",
|
||||||
|
prompt: "Pick one color.",
|
||||||
|
options: [{ value: "blue", label: "Blue" }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const lines: string[] = [];
|
||||||
|
renderSubmitScreen(
|
||||||
|
lines,
|
||||||
|
state,
|
||||||
|
plainTheme(),
|
||||||
|
UI_DIMENSIONS.submitWideMinWidth - 14,
|
||||||
|
"Press 1, 2, or 3 twice to confirm a review action."
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
lines.includes(" Press 1, 2, or 3 twice to confirm a review"),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
assert.equal(lines.includes(" action."), true);
|
||||||
|
});
|
||||||
|
|
||||||
test("submit screen shows notes only for answered questions in submit mode", () => {
|
test("submit screen shows notes only for answered questions in submit mode", () => {
|
||||||
let state = createInitialState({
|
let state = createInitialState({
|
||||||
questions: [
|
questions: [
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import test from "node:test";
|
||||||
|
import {
|
||||||
|
getReviewShortcutHint,
|
||||||
|
resolveReviewShortcutDoublePress,
|
||||||
|
} from "../src/ui/review-shortcuts.ts";
|
||||||
|
|
||||||
|
test("review shortcut double press requires the same digit twice", () => {
|
||||||
|
const first = resolveReviewShortcutDoublePress(2);
|
||||||
|
assert.equal(first.confirmed, false);
|
||||||
|
assert.equal(first.actionIndex, 1);
|
||||||
|
assert.equal(first.pendingActionIndex, 1);
|
||||||
|
|
||||||
|
const second = resolveReviewShortcutDoublePress(2, first.pendingActionIndex);
|
||||||
|
assert.equal(second.confirmed, true);
|
||||||
|
assert.equal(second.actionIndex, 1);
|
||||||
|
assert.equal(second.pendingActionIndex, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("review shortcut double press switches pending action when digit changes", () => {
|
||||||
|
const first = resolveReviewShortcutDoublePress(1);
|
||||||
|
const second = resolveReviewShortcutDoublePress(3, first.pendingActionIndex);
|
||||||
|
|
||||||
|
assert.equal(second.confirmed, false);
|
||||||
|
assert.equal(second.actionIndex, 2);
|
||||||
|
assert.equal(second.pendingActionIndex, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("review shortcut hint reflects pending confirmation state", () => {
|
||||||
|
assert.equal(
|
||||||
|
getReviewShortcutHint(),
|
||||||
|
"Press 1, 2, or 3 twice to confirm a review action."
|
||||||
|
);
|
||||||
|
assert.equal(getReviewShortcutHint(1), "Press 2 again to Elaborate.");
|
||||||
|
});
|
||||||
@@ -7,7 +7,8 @@ import { AskSettingsModal } from "../src/ui/settings-modal.ts";
|
|||||||
const savedConfig: AskConfig = {
|
const savedConfig: AskConfig = {
|
||||||
behaviour: {
|
behaviour: {
|
||||||
autoSubmitWhenAnsweredWithoutNotes: false,
|
autoSubmitWhenAnsweredWithoutNotes: false,
|
||||||
confirmDismissWhenDirty: false,
|
confirmDismissWhenDirty: true,
|
||||||
|
doublePressReviewShortcuts: true,
|
||||||
showFooterHints: true,
|
showFooterHints: true,
|
||||||
},
|
},
|
||||||
keymaps: {
|
keymaps: {
|
||||||
@@ -59,12 +60,15 @@ function createModal(
|
|||||||
test("settings modal renders compactly on phone-width screens", () => {
|
test("settings modal renders compactly on phone-width screens", () => {
|
||||||
const modal = createModal();
|
const modal = createModal();
|
||||||
const lines = modal.render(28);
|
const lines = modal.render(28);
|
||||||
|
const text = lines.join("\n");
|
||||||
|
|
||||||
assert(lines.every((line) => visibleWidth(line) <= 28));
|
assert(lines.every((line) => visibleWidth(line) <= 28));
|
||||||
assert(lines.join("\n").includes("@eko24ive/pi-ask"));
|
assert(lines.length <= 24);
|
||||||
|
assert(text.includes("@eko24ive/pi-ask"));
|
||||||
assert(lines[1]?.includes("Keymaps"));
|
assert(lines[1]?.includes("Keymaps"));
|
||||||
assert(lines.join("\n").includes("/reload after editing."));
|
assert(text.includes("Edit keymaps there, then"));
|
||||||
assert(lines.join("\n").includes("Open ask settings modal"));
|
assert(text.includes("? settings"));
|
||||||
|
assert(text.includes("Esc cancel"));
|
||||||
assert.equal(lines[0]?.includes("├"), false);
|
assert.equal(lines[0]?.includes("├"), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -77,12 +81,23 @@ test("settings modal renders behaviour tab with checkbox and dirty notice", () =
|
|||||||
|
|
||||||
assert(text.includes("Behaviour"));
|
assert(text.includes("Behaviour"));
|
||||||
assert(text.includes("[x] Auto-submit when answered without notes"));
|
assert(text.includes("[x] Auto-submit when answered without notes"));
|
||||||
assert(text.includes("[ ] Confirm dismiss when dirty"));
|
assert(text.includes("[x] Confirm dismiss when dirty"));
|
||||||
|
assert(text.includes("[x] Double-press review shortcuts"));
|
||||||
assert(text.includes("[x] Show footer hints"));
|
assert(text.includes("[x] Show footer hints"));
|
||||||
assert(text.includes("Unsaved changes. Press Ctrl+S to save."));
|
assert(text.includes("Unsaved changes. Press Ctrl+S to save."));
|
||||||
assert(text.includes("Ctrl+S save · Esc/Ctrl+C/? twice discard"));
|
assert(text.includes("Ctrl+S save · Esc/Ctrl+C/? twice discard"));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("settings modal keeps behaviour tab compact on narrow screens", () => {
|
||||||
|
const modal = createModal();
|
||||||
|
modal.handleInput("\t");
|
||||||
|
const lines = modal.render(36);
|
||||||
|
|
||||||
|
assert(lines.every((line) => visibleWidth(line) <= 36));
|
||||||
|
assert(lines.length <= 21);
|
||||||
|
assert(lines.join("\n").includes("Show footer hints"));
|
||||||
|
});
|
||||||
|
|
||||||
test("settings modal keymaps tab shows invalid-keymaps session warning", () => {
|
test("settings modal keymaps tab shows invalid-keymaps session warning", () => {
|
||||||
const modal = new AskSettingsModal(plainTheme(), {
|
const modal = new AskSettingsModal(plainTheme(), {
|
||||||
configPath: "/tmp/eko24ive-pi-ask.json",
|
configPath: "/tmp/eko24ive-pi-ask.json",
|
||||||
@@ -135,7 +150,8 @@ test("settings modal saves and clears dirty state", async () => {
|
|||||||
|
|
||||||
const text = modal.render(72).join("\n");
|
const text = modal.render(72).join("\n");
|
||||||
assert.equal(saved?.behaviour.autoSubmitWhenAnsweredWithoutNotes, true);
|
assert.equal(saved?.behaviour.autoSubmitWhenAnsweredWithoutNotes, true);
|
||||||
assert.equal(saved?.behaviour.confirmDismissWhenDirty, false);
|
assert.equal(saved?.behaviour.confirmDismissWhenDirty, true);
|
||||||
|
assert.equal(saved?.behaviour.doublePressReviewShortcuts, true);
|
||||||
assert.equal(saved?.behaviour.showFooterHints, true);
|
assert.equal(saved?.behaviour.showFooterHints, true);
|
||||||
assert(text.includes("Saved"));
|
assert(text.includes("Saved"));
|
||||||
assert.equal(text.includes("Unsaved changes. Press Ctrl+S to save."), false);
|
assert.equal(text.includes("Unsaved changes. Press Ctrl+S to save."), false);
|
||||||
|
|||||||
Reference in New Issue
Block a user