feat: add ask settings modal shell
This commit is contained in:
@@ -93,7 +93,7 @@ Ask the agent to elaborate on notes before finalizing choices.
|
|||||||
|
|
||||||
| Key | Context | Effect |
|
| Key | Context | Effect |
|
||||||
|-----------------------------|-----------------------------------------|---------------------------------------------|
|
|-----------------------------|-----------------------------------------|---------------------------------------------|
|
||||||
| `?` | Ask flow / empty editor | Open key bindings modal |
|
| `?` | Ask flow / empty editor | Open ask settings modal |
|
||||||
| `Tab` `Shift+Tab` | Main flow | Switch tabs |
|
| `Tab` `Shift+Tab` | Main flow | Switch tabs |
|
||||||
| `←` `→` | Main flow | Switch tabs |
|
| `←` `→` | Main flow | Switch tabs |
|
||||||
| `↑` `↓` | Main flow | Move cursor |
|
| `↑` `↓` | Main flow | Move cursor |
|
||||||
@@ -116,9 +116,9 @@ Ask the agent to elaborate on notes before finalizing choices.
|
|||||||
|
|
||||||
## Use
|
## Use
|
||||||
|
|
||||||
After installation, the extension registers the `ask_user` tool.
|
After installation, the extension registers the `ask_user` tool and `/ask-settings` command.
|
||||||
|
|
||||||
Agents can auto-discover and call it 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.
|
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. `/ask-settings` opens the same modal used by `?`, with `Keymaps` and `Behaviour` tabs.
|
||||||
|
|
||||||
This package also bundles the `ask-user` skill profile from `skills/ask-user/SKILL.md`. It reinforces when to use the tool, is enabled by default when installed, and can be disabled via `pi config`. The skill was inspired by https://github.com/edlsh/pi-ask-user.
|
This package also bundles the `ask-user` skill profile from `skills/ask-user/SKILL.md`. It reinforces when to use the tool, is enabled by default when installed, and can be disabled via `pi config`. The skill was inspired by https://github.com/edlsh/pi-ask-user.
|
||||||
|
|
||||||
|
|||||||
+4
-2
@@ -199,13 +199,15 @@ This document defines the stable external behavior. It does not explain internal
|
|||||||
- on the review tab, `Submit` and `Cancel` preview notes only for answered questions
|
- on the review tab, `Submit` and `Cancel` preview notes only for answered questions
|
||||||
- on the review tab, `Elaborate` preview expands to all question notes and all option notes, including notes on unselected options
|
- on the review tab, `Elaborate` preview expands to all question notes and all option notes, including notes on unselected options
|
||||||
- transcript-friendly call and result rendering
|
- transcript-friendly call and result rendering
|
||||||
|
- ask settings modal with `Keymaps` and `Behaviour` tabs; `Behaviour` may be empty while unimplemented
|
||||||
|
- `/ask-settings` opens the same ask settings modal as `?`
|
||||||
- elaborate results are phrased as direct follow-up instructions, for example: `User asked to elaborate on question "Which option would you like to select?" option "Option A" with note "why this one?"`
|
- elaborate results are phrased as direct follow-up instructions, for example: `User asked to elaborate on question "Which option would you like to select?" option "Option A" with note "why this one?"`
|
||||||
|
|
||||||
## Keyboard behavior
|
## Keyboard behavior
|
||||||
|
|
||||||
Main flow:
|
Main flow:
|
||||||
|
|
||||||
- `?`: open the key bindings modal
|
- `?`: open the ask settings modal on the `Keymaps` tab
|
||||||
- `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
|
||||||
- `Enter`: confirm or submit
|
- `Enter`: confirm or submit
|
||||||
@@ -221,7 +223,7 @@ Editing flow:
|
|||||||
- `Enter`: submit the current editor input and close the editor; in note editors this saves the note only and keeps the ask flow open
|
- `Enter`: submit the current editor input and close the editor; in note editors this saves the note only and keeps the ask flow open
|
||||||
- `Esc`: save draft and close the editor
|
- `Esc`: save draft and close the editor
|
||||||
- `Ctrl+C`: dismiss the entire flow immediately without saving the current editor draft
|
- `Ctrl+C`: dismiss the entire flow immediately without saving the current editor draft
|
||||||
- `?`: open the key bindings modal when the editor is empty; otherwise enter `?` as text
|
- `?`: open the ask settings modal on the `Keymaps` tab when the editor is empty; otherwise enter `?` as text
|
||||||
- when editor has text, arrow keys and `Tab` stay in the editor so the cursor can move while typing
|
- when editor has text, arrow keys and `Tab` stay in the editor so the cursor can move while typing
|
||||||
- when editor is empty, `Up`/`Down` move options and `Tab`/`Shift+Tab`/`Left`/`Right` move between tabs without requiring `Esc` first
|
- when editor is empty, `Up`/`Down` move options and `Tab`/`Shift+Tab`/`Left`/`Right` move between tabs without requiring `Esc` first
|
||||||
- navigation resumes only after closing the editor with `Esc`, unless the editor is empty and the navigation keys above are used
|
- navigation resumes only after closing the editor with `Esc`, unless the editor is empty and the navigation keys above are used
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import type {
|
||||||
|
ExtensionAPI,
|
||||||
|
ExtensionCommandContext,
|
||||||
|
} from "@mariozechner/pi-coding-agent";
|
||||||
|
import { showAskSettingsModal } from "./ui/show-settings-modal.ts";
|
||||||
|
|
||||||
|
export function registerAskSettingsCommand(pi: ExtensionAPI) {
|
||||||
|
pi.registerCommand("ask-settings", {
|
||||||
|
description: "Open the ask settings modal",
|
||||||
|
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
||||||
|
await showAskSettingsModal(ctx.ui);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
||||||
|
import { registerAskSettingsCommand } from "./ask-settings-command.ts";
|
||||||
import { registerAskTool } from "./ask-tool.ts";
|
import { registerAskTool } from "./ask-tool.ts";
|
||||||
|
|
||||||
export default function askExtension(pi: ExtensionAPI) {
|
export default function askExtension(pi: ExtensionAPI) {
|
||||||
registerAskTool(pi);
|
registerAskTool(pi);
|
||||||
|
registerAskSettingsCommand(pi);
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-22
@@ -27,10 +27,10 @@ import {
|
|||||||
import { isEditingView } from "../state/view.ts";
|
import { isEditingView } from "../state/view.ts";
|
||||||
import type { AskParams, AskResult, AskState } from "../types.ts";
|
import type { AskParams, AskResult, AskState } from "../types.ts";
|
||||||
import { createAskAutocompleteProvider } from "./autocomplete.ts";
|
import { createAskAutocompleteProvider } from "./autocomplete.ts";
|
||||||
import { AskHelpModal } from "./help-modal.ts";
|
|
||||||
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 { showAskSettingsModal } from "./show-settings-modal.ts";
|
||||||
|
|
||||||
type CustomCallback = Parameters<ExtensionContext["ui"]["custom"]>[0];
|
type CustomCallback = Parameters<ExtensionContext["ui"]["custom"]>[0];
|
||||||
type CustomCallbackArgs = CustomCallback extends (...args: infer T) => unknown
|
type CustomCallbackArgs = CustomCallback extends (...args: infer T) => unknown
|
||||||
@@ -288,27 +288,10 @@ function showHelpModal(controller: AskFlowController) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
controller.helpOpen = true;
|
controller.helpOpen = true;
|
||||||
controller.ctx.ui
|
showAskSettingsModal(controller.ctx.ui).finally(() => {
|
||||||
.custom<void>(
|
controller.helpOpen = false;
|
||||||
(_tui, _theme, _keybindings, done) =>
|
refresh(controller);
|
||||||
new AskHelpModal(controller.theme, () => {
|
});
|
||||||
done();
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
overlay: true,
|
|
||||||
overlayOptions: {
|
|
||||||
anchor: "center",
|
|
||||||
margin: 1,
|
|
||||||
maxHeight: "90%",
|
|
||||||
minWidth: 26,
|
|
||||||
width: 72,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.finally(() => {
|
|
||||||
controller.helpOpen = false;
|
|
||||||
refresh(controller);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function refresh(controller: AskFlowController) {
|
function refresh(controller: AskFlowController) {
|
||||||
|
|||||||
@@ -1,101 +0,0 @@
|
|||||||
import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
|
|
||||||
import {
|
|
||||||
ASK_HELP_CLOSE_BINDINGS,
|
|
||||||
ASK_HELP_CLOSE_HINT,
|
|
||||||
ASK_KEYMAPS,
|
|
||||||
matchesBinding,
|
|
||||||
} from "../constants/keymaps.ts";
|
|
||||||
import { wrapText } from "../text.ts";
|
|
||||||
|
|
||||||
interface Theme {
|
|
||||||
bg(color: string, text: string): string;
|
|
||||||
fg(color: string, text: string): string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const MIN_WIDTH = 24;
|
|
||||||
const COMPACT_WIDTH = 46;
|
|
||||||
|
|
||||||
function padToWidth(text: string, width: number): string {
|
|
||||||
return text + " ".repeat(Math.max(0, width - visibleWidth(text)));
|
|
||||||
}
|
|
||||||
|
|
||||||
export class AskHelpModal {
|
|
||||||
private closed = false;
|
|
||||||
private readonly onClose: () => void;
|
|
||||||
private readonly theme: Theme;
|
|
||||||
|
|
||||||
constructor(theme: Theme, onClose: () => void) {
|
|
||||||
this.theme = theme;
|
|
||||||
this.onClose = onClose;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleInput(data: string): void {
|
|
||||||
if (
|
|
||||||
ASK_HELP_CLOSE_BINDINGS.some((binding) => matchesBinding(data, binding))
|
|
||||||
) {
|
|
||||||
this.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render(width: number): string[] {
|
|
||||||
const innerW = Math.max(MIN_WIDTH, width - 2);
|
|
||||||
const compact = innerW < COMPACT_WIDTH;
|
|
||||||
const keyW = compact ? 11 : 16;
|
|
||||||
const descW = Math.max(8, innerW - keyW - 5);
|
|
||||||
const border = (text: string) => this.theme.fg("border", text);
|
|
||||||
const title = this.theme.fg("accent", " @eko24ive/pi-ask ");
|
|
||||||
const titleW = visibleWidth(title);
|
|
||||||
const titlePad = Math.max(0, innerW - titleW);
|
|
||||||
const left = Math.floor(titlePad / 2);
|
|
||||||
const right = titlePad - left;
|
|
||||||
const line = (content = "") =>
|
|
||||||
`${border("│")}${padToWidth(` ${content}`, innerW)}${border("│")}`;
|
|
||||||
|
|
||||||
const lines = [
|
|
||||||
`${border(`╭${"─".repeat(left)}`)}${title}${border(`${"─".repeat(right)}╮`)}`,
|
|
||||||
`${border(`├${"─".repeat(innerW)}┤`)}`,
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const hint of ASK_KEYMAPS) {
|
|
||||||
if (compact) {
|
|
||||||
lines.push(line(this.theme.fg("accent", hint.label)));
|
|
||||||
for (const descLine of wrapText(hint.description, innerW - 3)) {
|
|
||||||
lines.push(line(` ${this.theme.fg("text", descLine)}`));
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const keyText = padToWidth(truncateToWidth(hint.label, keyW, "…"), keyW);
|
|
||||||
const key = this.theme.fg("accent", keyText);
|
|
||||||
const desc = truncateToWidth(hint.description, descW, "…");
|
|
||||||
lines.push(line(`${key} ${desc}`));
|
|
||||||
const contexts = hint.contexts.join(", ");
|
|
||||||
lines.push(
|
|
||||||
line(`${" ".repeat(keyW)} ${this.theme.fg("dim", contexts)}`)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
lines.push(`${border(`├${"─".repeat(innerW)}┤`)}`);
|
|
||||||
for (const hintLine of wrapText(ASK_HELP_CLOSE_HINT, innerW - 2)) {
|
|
||||||
lines.push(line(this.theme.fg("muted", hintLine)));
|
|
||||||
}
|
|
||||||
lines.push(`${border(`╰${"─".repeat(innerW)}╯`)}`);
|
|
||||||
return lines;
|
|
||||||
}
|
|
||||||
|
|
||||||
invalidate(): void {
|
|
||||||
// Static modal; no async invalidation needed.
|
|
||||||
}
|
|
||||||
|
|
||||||
dispose(): void {
|
|
||||||
this.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
private close(): void {
|
|
||||||
if (this.closed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.closed = true;
|
|
||||||
this.onClose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
|
||||||
|
import {
|
||||||
|
ASK_HELP_CLOSE_BINDINGS,
|
||||||
|
ASK_HELP_CLOSE_HINT,
|
||||||
|
ASK_KEY_BINDINGS,
|
||||||
|
ASK_KEYMAPS,
|
||||||
|
matchesBinding,
|
||||||
|
} from "../constants/keymaps.ts";
|
||||||
|
import { wrapText } from "../text.ts";
|
||||||
|
|
||||||
|
type SettingsTab = "keymaps" | "behaviour";
|
||||||
|
|
||||||
|
interface Theme {
|
||||||
|
bg(color: string, text: string): string;
|
||||||
|
fg(color: string, text: string): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MIN_WIDTH = 24;
|
||||||
|
const COMPACT_WIDTH = 46;
|
||||||
|
const TAB_GAP = " ";
|
||||||
|
|
||||||
|
function padToWidth(text: string, width: number): string {
|
||||||
|
return text + " ".repeat(Math.max(0, width - visibleWidth(text)));
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AskSettingsModal {
|
||||||
|
private activeTab: SettingsTab = "keymaps";
|
||||||
|
private closed = false;
|
||||||
|
private readonly onClose: () => void;
|
||||||
|
private readonly theme: Theme;
|
||||||
|
|
||||||
|
constructor(theme: Theme, onClose: () => void) {
|
||||||
|
this.theme = theme;
|
||||||
|
this.onClose = onClose;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleInput(data: string): void {
|
||||||
|
if (
|
||||||
|
ASK_HELP_CLOSE_BINDINGS.some((binding) => matchesBinding(data, binding))
|
||||||
|
) {
|
||||||
|
this.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (matchesBinding(data, ASK_KEY_BINDINGS.nextTab)) {
|
||||||
|
this.activeTab = this.activeTab === "keymaps" ? "behaviour" : "keymaps";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (matchesBinding(data, ASK_KEY_BINDINGS.previousTab)) {
|
||||||
|
this.activeTab = this.activeTab === "behaviour" ? "keymaps" : "behaviour";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render(width: number): string[] {
|
||||||
|
const layout = createLayout(this.theme, this.activeTab, width);
|
||||||
|
const lines = [...layout.headerLines];
|
||||||
|
|
||||||
|
if (this.activeTab === "keymaps") {
|
||||||
|
lines.push(...renderKeymapsTab(this.theme, layout));
|
||||||
|
} else {
|
||||||
|
lines.push(...renderBehaviourTab(this.theme, layout));
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push(`${layout.border(`├${"─".repeat(layout.innerW)}┤`)}`);
|
||||||
|
for (const hintLine of wrapText(ASK_HELP_CLOSE_HINT, layout.innerW - 2)) {
|
||||||
|
lines.push(layout.line(this.theme.fg("muted", hintLine)));
|
||||||
|
}
|
||||||
|
lines.push(`${layout.border(`╰${"─".repeat(layout.innerW)}╯`)}`);
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
invalidate(): void {
|
||||||
|
// Static modal; no async invalidation needed.
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose(): void {
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private close(): void {
|
||||||
|
if (this.closed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.closed = true;
|
||||||
|
this.onClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SettingsLayout {
|
||||||
|
border: (text: string) => string;
|
||||||
|
descW: number;
|
||||||
|
headerLines: string[];
|
||||||
|
innerW: number;
|
||||||
|
keyW: number;
|
||||||
|
line: (content?: string) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createLayout(
|
||||||
|
theme: Theme,
|
||||||
|
activeTab: SettingsTab,
|
||||||
|
width: number
|
||||||
|
): SettingsLayout {
|
||||||
|
const innerW = Math.max(MIN_WIDTH, width - 2);
|
||||||
|
const compact = innerW < COMPACT_WIDTH;
|
||||||
|
const keyW = compact ? 11 : 16;
|
||||||
|
const descW = Math.max(8, innerW - keyW - 5);
|
||||||
|
const border = (text: string) => theme.fg("border", text);
|
||||||
|
const title = theme.fg("accent", " @eko24ive/pi-ask ");
|
||||||
|
const titleW = visibleWidth(title);
|
||||||
|
const titlePad = Math.max(0, innerW - titleW);
|
||||||
|
const left = Math.floor(titlePad / 2);
|
||||||
|
const right = titlePad - left;
|
||||||
|
const line = (content = "") =>
|
||||||
|
`${border("│")}${padToWidth(` ${content}`, innerW)}${border("│")}`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
border,
|
||||||
|
descW,
|
||||||
|
headerLines: [
|
||||||
|
`${border(`╭${"─".repeat(left)}`)}${title}${border(`${"─".repeat(right)}╮`)}`,
|
||||||
|
line(renderTabs(theme, activeTab, innerW - 1)),
|
||||||
|
line(),
|
||||||
|
],
|
||||||
|
innerW,
|
||||||
|
keyW,
|
||||||
|
line,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderKeymapsTab(theme: Theme, layout: SettingsLayout): string[] {
|
||||||
|
const lines: string[] = [];
|
||||||
|
const compact = layout.innerW < COMPACT_WIDTH;
|
||||||
|
|
||||||
|
for (const hint of ASK_KEYMAPS) {
|
||||||
|
if (compact) {
|
||||||
|
lines.push(layout.line(theme.fg("accent", hint.label)));
|
||||||
|
for (const descLine of wrapText(hint.description, layout.innerW - 3)) {
|
||||||
|
lines.push(layout.line(` ${theme.fg("text", descLine)}`));
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyText = padToWidth(
|
||||||
|
truncateToWidth(hint.label, layout.keyW, "…"),
|
||||||
|
layout.keyW
|
||||||
|
);
|
||||||
|
const key = theme.fg("accent", keyText);
|
||||||
|
const desc = truncateToWidth(hint.description, layout.descW, "…");
|
||||||
|
lines.push(layout.line(`${key} ${desc}`));
|
||||||
|
const contexts = hint.contexts.join(", ");
|
||||||
|
lines.push(
|
||||||
|
layout.line(`${" ".repeat(layout.keyW)} ${theme.fg("dim", contexts)}`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderBehaviourTab(theme: Theme, layout: SettingsLayout): string[] {
|
||||||
|
return wrapText("Behaviour settings coming soon.", layout.innerW - 2).map(
|
||||||
|
(settingsLine) => layout.line(theme.fg("muted", settingsLine))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTabs(
|
||||||
|
theme: Theme,
|
||||||
|
activeTab: SettingsTab,
|
||||||
|
width: number
|
||||||
|
): string {
|
||||||
|
const renderTab = (tab: SettingsTab, label: string) => {
|
||||||
|
const text = ` ${label} `;
|
||||||
|
return activeTab === tab
|
||||||
|
? theme.bg("selectedBg", theme.fg("accent", text))
|
||||||
|
: theme.fg("muted", text);
|
||||||
|
};
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
renderTab("keymaps", "Keymaps"),
|
||||||
|
renderTab("behaviour", "Behaviour"),
|
||||||
|
].join(TAB_GAP);
|
||||||
|
return truncateToWidth(tabs, width, "");
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
||||||
|
import { AskSettingsModal } from "./settings-modal.ts";
|
||||||
|
|
||||||
|
export function showAskSettingsModal(
|
||||||
|
ui: Pick<ExtensionContext, "ui">["ui"]
|
||||||
|
): Promise<void> {
|
||||||
|
return ui.custom<void>(
|
||||||
|
(_tui, theme, _keybindings, done) =>
|
||||||
|
new AskSettingsModal(theme, () => {
|
||||||
|
done();
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
overlay: true,
|
||||||
|
overlayOptions: {
|
||||||
|
anchor: "center",
|
||||||
|
margin: 1,
|
||||||
|
maxHeight: "90%",
|
||||||
|
minWidth: 26,
|
||||||
|
width: 72,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import test from "node:test";
|
||||||
|
import { registerAskSettingsCommand } from "../src/ask-settings-command.ts";
|
||||||
|
|
||||||
|
function plainTheme() {
|
||||||
|
return {
|
||||||
|
fg(_color: string, text: string) {
|
||||||
|
return text;
|
||||||
|
},
|
||||||
|
bg(_color: string, text: string) {
|
||||||
|
return text;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test("registers /ask-settings and opens the shared modal overlay", async () => {
|
||||||
|
const commands = new Map<
|
||||||
|
string,
|
||||||
|
{ handler: (args: string, ctx: any) => Promise<void> }
|
||||||
|
>();
|
||||||
|
registerAskSettingsCommand({
|
||||||
|
registerCommand(
|
||||||
|
name: string,
|
||||||
|
command: { handler: (args: string, ctx: any) => Promise<void> }
|
||||||
|
) {
|
||||||
|
commands.set(name, command);
|
||||||
|
},
|
||||||
|
} as never);
|
||||||
|
|
||||||
|
assert(commands.has("ask-settings"));
|
||||||
|
|
||||||
|
const customCalls: Array<{ options: unknown; lines: string[] }> = [];
|
||||||
|
await commands.get("ask-settings")?.handler("", {
|
||||||
|
ui: {
|
||||||
|
custom(callback: any, options: unknown) {
|
||||||
|
const done = () => {
|
||||||
|
// test callback intentionally unused
|
||||||
|
};
|
||||||
|
const component = callback(undefined, plainTheme(), undefined, done);
|
||||||
|
customCalls.push({ options, lines: component.render(72) });
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(customCalls.length, 1);
|
||||||
|
assert.deepEqual(customCalls[0]?.options, {
|
||||||
|
overlay: true,
|
||||||
|
overlayOptions: {
|
||||||
|
anchor: "center",
|
||||||
|
margin: 1,
|
||||||
|
maxHeight: "90%",
|
||||||
|
minWidth: 26,
|
||||||
|
width: 72,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
assert(customCalls[0]?.lines.join("\n").includes("Keymaps"));
|
||||||
|
});
|
||||||
+32
-14
@@ -2,7 +2,7 @@ import assert from "node:assert/strict";
|
|||||||
import test from "node:test";
|
import test from "node:test";
|
||||||
import { visibleWidth } from "@mariozechner/pi-tui";
|
import { visibleWidth } from "@mariozechner/pi-tui";
|
||||||
import { ASK_HELP_CLOSE_HINT, ASK_KEYMAPS } from "../src/constants/keymaps.ts";
|
import { ASK_HELP_CLOSE_HINT, ASK_KEYMAPS } from "../src/constants/keymaps.ts";
|
||||||
import { AskHelpModal } from "../src/ui/help-modal.ts";
|
import { AskSettingsModal } from "../src/ui/settings-modal.ts";
|
||||||
|
|
||||||
const noop = () => {
|
const noop = () => {
|
||||||
// Test callback intentionally does nothing.
|
// Test callback intentionally does nothing.
|
||||||
@@ -19,20 +19,23 @@ function plainTheme() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
test("help modal renders compactly on phone-width screens", () => {
|
test("settings modal renders compactly on phone-width screens", () => {
|
||||||
const modal = new AskHelpModal(plainTheme(), noop);
|
const modal = new AskSettingsModal(plainTheme(), noop);
|
||||||
const lines = modal.render(28);
|
const lines = modal.render(28);
|
||||||
|
|
||||||
assert(lines.length > ASK_KEYMAPS.length + 4);
|
assert(lines.length > ASK_KEYMAPS.length + 4);
|
||||||
assert(lines.every((line) => visibleWidth(line) <= 28));
|
assert(lines.every((line) => visibleWidth(line) <= 28));
|
||||||
assert(lines.join("\n").includes("@eko24ive/pi-ask"));
|
assert(lines.join("\n").includes("@eko24ive/pi-ask"));
|
||||||
assert(lines.slice(-3, -1).join(" ").includes("Esc, Ctrl+C, or ?"));
|
assert(lines.slice(-3, -1).join(" ").includes("Esc, Ctrl+C, or ?"));
|
||||||
|
assert(lines[1]?.includes("Keymaps"));
|
||||||
|
assert.equal(lines[2], "│ │");
|
||||||
assert(lines.includes("│ ? │"));
|
assert(lines.includes("│ ? │"));
|
||||||
assert(lines.includes("│ Show this menu │"));
|
assert(lines.includes("│ Show this menu │"));
|
||||||
|
assert.equal(lines[0]?.includes("├"), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("help modal keeps context rows on wider screens", () => {
|
test("settings modal keeps context rows on wider screens", () => {
|
||||||
const modal = new AskHelpModal(plainTheme(), noop);
|
const modal = new AskSettingsModal(plainTheme(), noop);
|
||||||
const lines = modal.render(72);
|
const lines = modal.render(72);
|
||||||
|
|
||||||
assert(lines.length > ASK_KEYMAPS.length + 4);
|
assert(lines.length > ASK_KEYMAPS.length + 4);
|
||||||
@@ -40,22 +43,37 @@ test("help modal keeps context rows on wider screens", () => {
|
|||||||
assert(lines.at(-2)?.includes(ASK_HELP_CLOSE_HINT));
|
assert(lines.at(-2)?.includes(ASK_HELP_CLOSE_HINT));
|
||||||
});
|
});
|
||||||
|
|
||||||
test("help modal closes from configured keyboard shortcuts", () => {
|
test("settings modal switches tabs and closes from configured shortcuts", () => {
|
||||||
|
let closed = 0;
|
||||||
|
const modal = new AskSettingsModal(plainTheme(), () => {
|
||||||
|
closed += 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
modal.handleInput("\t");
|
||||||
|
assert(
|
||||||
|
modal.render(72).join("\n").includes("Behaviour settings coming soon.")
|
||||||
|
);
|
||||||
|
assert(modal.render(72).join("\n").includes("Behaviour"));
|
||||||
|
|
||||||
|
modal.handleInput("\u001b[Z");
|
||||||
|
assert.equal(
|
||||||
|
modal.render(72).join("\n").includes("Behaviour settings coming soon."),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
for (const input of ["?", "\u001b", "\u0003"]) {
|
for (const input of ["?", "\u001b", "\u0003"]) {
|
||||||
let closed = 0;
|
const closable = new AskSettingsModal(plainTheme(), () => {
|
||||||
const modal = new AskHelpModal(plainTheme(), () => {
|
|
||||||
closed += 1;
|
closed += 1;
|
||||||
});
|
});
|
||||||
|
closable.handleInput(input);
|
||||||
modal.handleInput(input);
|
|
||||||
|
|
||||||
assert.equal(closed, 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assert.equal(closed, 3);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("help modal ignores enter and closes idempotently on dispose", () => {
|
test("settings modal ignores enter and closes idempotently on dispose", () => {
|
||||||
let closed = 0;
|
let closed = 0;
|
||||||
const modal = new AskHelpModal(plainTheme(), () => {
|
const modal = new AskSettingsModal(plainTheme(), () => {
|
||||||
closed += 1;
|
closed += 1;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user