@narumitw/pi-plan-mode 0.17.2 → 0.20.0
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 +4 -4
- package/package.json +3 -3
- package/src/plan-mode.ts +88 -26
- package/src/selector-ui.ts +95 -0
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ Pi core intentionally does not ship a built-in plan mode; this package provides
|
|
|
12
12
|
- Adds `--plan` to start a session in Plan mode.
|
|
13
13
|
- Enables built-in read-only tools by default while Plan mode is active.
|
|
14
14
|
- Disables extension and custom tools by default, with a `/plan tools` selector for explicit user-risk opt-in.
|
|
15
|
-
- Blocks `update_plan`, mutating built-in tools, and unsafe
|
|
15
|
+
- Blocks `update_plan`, mutating built-in tools, and unsafe `bash` forms such as writes, substitutions, background jobs, dependency installs, and mutating Git commands.
|
|
16
16
|
- Injects Codex-like Plan mode instructions: explore first, ask decision questions for high-impact ambiguity, do not mutate files, and finalize only when decision-complete.
|
|
17
17
|
- Adds required `plan_mode_question` and `plan_mode_complete` tools for structured questions and completion.
|
|
18
18
|
- Presents the complete plan and prompts you to implement, stay in Plan mode, or exit and discard it.
|
|
@@ -58,7 +58,7 @@ When Plan mode is active, ask the agent to design the change. The agent may insp
|
|
|
58
58
|
|
|
59
59
|
By default, Plan mode manages only Pi's built-in tools: `read`, limited `bash`, available read-only built-ins such as `grep`, `find`, and `ls`, plus the required `plan_mode_question` and `plan_mode_complete` tools. Built-in `edit` and `write` are blocked. `update_plan` is also blocked because it tracks execution progress rather than conversational planning. Extension and custom tools are disabled by default because Pi tools do not expose standardized mutability metadata; enable them from `/plan tools` only when you accept the risk for that session. For example, you can opt into `firecrawl_scrape`, `firecrawl_search`, or `lsp_diagnostics` if those extensions are loaded and you want to use them during planning.
|
|
60
60
|
|
|
61
|
-
Limited `bash` uses a fail-closed policy. It accepts common inspection commands, read-only Git and npm queries, pipelines and command lists composed entirely of accepted commands, plus selected checks such as `npm test`, `npm run typecheck`, and `cargo test`. It rejects output/input redirects, shell expansion, substitutions, subshells, background jobs, mutating flags, dependency changes, editors, and unknown commands. Tests and builds may still write ignored caches or build artifacts and may execute project-defined hooks; enable or invoke them only when the repository is trusted. This is extension-level risk reduction, not an OS sandbox.
|
|
61
|
+
Limited `bash` uses a fail-closed policy, including when an extension overrides the canonical `bash` tool name. It accepts common inspection commands, read-only Git and npm queries, pipelines and command lists composed entirely of accepted commands, plus selected checks such as `npm test`, `npm run typecheck`, and `cargo test`. It rejects output/input redirects, shell expansion, substitutions, subshells, background jobs, mutating flags, dependency changes, editors, and unknown commands. Tests and builds may still write ignored caches or build artifacts and may execute project-defined hooks; enable or invoke them only when the repository is trusted. This is extension-level risk reduction, not an OS sandbox.
|
|
62
62
|
|
|
63
63
|
`plan_mode_question` follows Codex's `request_user_input` pattern: the agent can ask 1-3 concise questions, each with meaningful options and a free-form Other path. If you cancel or no interactive UI is available, the agent should ask a concise plain-text question or proceed only with a clearly stated low-risk assumption instead of prematurely producing a final plan.
|
|
64
64
|
|
|
@@ -102,7 +102,7 @@ Create `$PI_CODING_AGENT_DIR/pi-plan-mode.json` (normally `~/.pi/agent/pi-plan-m
|
|
|
102
102
|
|
|
103
103
|
`defaultPlanTools` defines the initial tool selection when a session has no stored `/plan tools` selection. Omit it to keep the available safe built-ins as the default. An explicit empty array is valid and enables only the required `plan_mode_question` and `plan_mode_complete` tools.
|
|
104
104
|
|
|
105
|
-
Tool names must be non-empty strings; duplicates are removed in first-seen order. Unknown, unavailable, and Plan-mode-blocked names are ignored when tools are activated. A tool registered after Plan mode is already active is not added automatically; re-enter Plan mode or reopen `/plan tools` to reapply the selection. Non-built-in tools named in this global setting are an explicit user-risk opt-in, just like selecting them with `/plan tools`. Pi resolves tools by name, so if an extension overrides a built-in name, the effective extension tool is selected instead.
|
|
105
|
+
Tool names must be non-empty strings; duplicates are removed in first-seen order. Unknown, unavailable, and Plan-mode-blocked names are ignored when tools are activated. A tool registered after Plan mode is already active is not added automatically; re-enter Plan mode or reopen `/plan tools` to reapply the selection. Non-built-in tools named in this global setting are an explicit user-risk opt-in, just like selecting them with `/plan tools`. Pi resolves tools by name, so if an extension overrides a built-in name, the effective extension tool is selected instead. An effective tool named `bash` remains subject to the limited-shell policy regardless of its source metadata.
|
|
106
106
|
|
|
107
107
|
A selection made with `/plan tools` is stored in that Pi session and takes precedence over `defaultPlanTools` when the session resumes. The global setting remains the baseline for fresh sessions and sessions without an explicit selection.
|
|
108
108
|
|
|
@@ -160,7 +160,7 @@ This extension maps Codex's `ModeKind::Plan` behavior onto Pi's extension API:
|
|
|
160
160
|
- The agent completes with a standalone `plan_mode_complete` tool call instead of relying on semantic prose detection.
|
|
161
161
|
- `update_plan` checklist use is blocked while Plan mode is active.
|
|
162
162
|
- The implementation boundary is explicit: Plan mode restores tools before starting implementation, choosing implementation immediately triggers a normal agent turn with full tool access, and plain exit/off discards the stored plan.
|
|
163
|
-
- Pi extension safety is approximated with tool classification and fail-closed bash
|
|
163
|
+
- Pi extension safety is approximated with tool classification and fail-closed filtering for every effective tool named `bash`; other non-built-in tools remain user-selected at user risk because Pi does not expose standardized tool mutability metadata.
|
|
164
164
|
- Unlike native Codex, this extension uses a terminating Pi tool plus an `agent_settled` ready flow; Pi cannot provide sandbox-level enforcement.
|
|
165
165
|
|
|
166
166
|
## 🗂️ Package layout
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@narumitw/pi-plan-mode",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"description": "Pi extension that adds a Codex-like read-only /plan collaboration mode.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@biomejs/biome": "2.5.3",
|
|
32
|
-
"@earendil-works/pi-coding-agent": "0.80.
|
|
33
|
-
"typescript": "
|
|
32
|
+
"@earendil-works/pi-coding-agent": "0.80.10",
|
|
33
|
+
"typescript": "7.0.2"
|
|
34
34
|
},
|
|
35
35
|
"repository": {
|
|
36
36
|
"type": "git",
|
package/src/plan-mode.ts
CHANGED
|
@@ -23,8 +23,10 @@ import {
|
|
|
23
23
|
planModeQuestionAnswered,
|
|
24
24
|
planModeQuestionCancelled,
|
|
25
25
|
} from "./question-tool.js";
|
|
26
|
+
import { showPersistentSelector } from "./selector-ui.js";
|
|
26
27
|
import {
|
|
27
28
|
configuredThinkingLevel,
|
|
29
|
+
type PlanModeFixedThinkingLevel,
|
|
28
30
|
type PlanModeSettings,
|
|
29
31
|
readPlanModeSettings,
|
|
30
32
|
} from "./settings.js";
|
|
@@ -46,6 +48,20 @@ const BLOCKED_BUILTIN_TOOLS = new Set(["edit", "write"]);
|
|
|
46
48
|
const DEFAULT_TOOLS = ["read", "bash", "edit", "write"];
|
|
47
49
|
const TOOL_SELECTOR_PAGE_SIZE = 10;
|
|
48
50
|
|
|
51
|
+
type AgentSettledHandler = (event: unknown, ctx: ExtensionContext) => unknown;
|
|
52
|
+
|
|
53
|
+
function onAgentSettled(pi: ExtensionAPI, handler: AgentSettledHandler) {
|
|
54
|
+
(
|
|
55
|
+
pi as unknown as {
|
|
56
|
+
on(event: "agent_settled", callback: AgentSettledHandler): void;
|
|
57
|
+
}
|
|
58
|
+
).on("agent_settled", handler);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function setPlanThinkingLevel(pi: ExtensionAPI, level: PlanModeFixedThinkingLevel) {
|
|
62
|
+
(pi.setThinkingLevel as unknown as (level: PlanModeFixedThinkingLevel) => void)(level);
|
|
63
|
+
}
|
|
64
|
+
|
|
49
65
|
interface CommandArgumentCompletion {
|
|
50
66
|
value: string;
|
|
51
67
|
label: string;
|
|
@@ -58,6 +74,10 @@ interface ReadyPresentationIntent {
|
|
|
58
74
|
source: PlanCompletionSource;
|
|
59
75
|
}
|
|
60
76
|
|
|
77
|
+
type PlanToolSelectorValue =
|
|
78
|
+
| { kind: "tool"; tool: ToolInfo }
|
|
79
|
+
| { kind: "action"; action: "previous" | "next" | "done" };
|
|
80
|
+
|
|
61
81
|
const PLAN_COMMAND_COMPLETIONS: readonly CommandArgumentCompletion[] = [
|
|
62
82
|
{ value: "show", label: "show", description: "Show the completed plan" },
|
|
63
83
|
{ value: "finalize", label: "finalize", description: "Request a completed plan" },
|
|
@@ -256,7 +276,8 @@ export default function planMode(pi: ExtensionAPI) {
|
|
|
256
276
|
reason: `Plan mode blocks built-in tool '${event.toolName}' because its metadata is unavailable.`,
|
|
257
277
|
};
|
|
258
278
|
}
|
|
259
|
-
|
|
279
|
+
// Built-in-compatible overrides retain the canonical name but replace its source metadata.
|
|
280
|
+
if (event.toolName !== "bash") return;
|
|
260
281
|
|
|
261
282
|
const command = readCommand(event.input);
|
|
262
283
|
if (!isSafeCommand(command, settings.safeSubcommands)) {
|
|
@@ -316,7 +337,7 @@ export default function planMode(pi: ExtensionAPI) {
|
|
|
316
337
|
acceptCompletedPlan(parsedPlan.plan, "legacy_proposed_plan", ctx);
|
|
317
338
|
});
|
|
318
339
|
|
|
319
|
-
pi
|
|
340
|
+
onAgentSettled(pi, async (_event, ctx) => {
|
|
320
341
|
const intent = readyPresentationIntent;
|
|
321
342
|
if (!intent || !readyPresentationIsCurrent(intent)) return;
|
|
322
343
|
if (!ctx.isIdle() || ctx.hasPendingMessages()) return;
|
|
@@ -547,6 +568,56 @@ export default function planMode(pi: ExtensionAPI) {
|
|
|
547
568
|
return;
|
|
548
569
|
}
|
|
549
570
|
|
|
571
|
+
const tools = selectableTools();
|
|
572
|
+
const pageCount = toolSelectorPageCount(tools);
|
|
573
|
+
let pageIndex = 0;
|
|
574
|
+
const customHandled = await showPersistentSelector(
|
|
575
|
+
ctx,
|
|
576
|
+
() => {
|
|
577
|
+
pageIndex = Math.min(pageIndex, pageCount - 1);
|
|
578
|
+
const pageStart = pageIndex * TOOL_SELECTOR_PAGE_SIZE;
|
|
579
|
+
const pageTools = tools.slice(pageStart, pageStart + TOOL_SELECTOR_PAGE_SIZE);
|
|
580
|
+
const selectedNames = planModeSelectedNames(tools);
|
|
581
|
+
const rows: Array<{ value: PlanToolSelectorValue; label: string }> = pageTools.map(
|
|
582
|
+
(tool, index) => ({
|
|
583
|
+
value: { kind: "tool", tool },
|
|
584
|
+
label: formatToolChoice(tool, selectedNames.has(tool.name), pageStart + index),
|
|
585
|
+
}),
|
|
586
|
+
);
|
|
587
|
+
if (pageIndex > 0) {
|
|
588
|
+
rows.push({ value: { kind: "action", action: "previous" }, label: "Previous page" });
|
|
589
|
+
}
|
|
590
|
+
if (pageIndex < pageCount - 1) {
|
|
591
|
+
rows.push({ value: { kind: "action", action: "next" }, label: "Next page" });
|
|
592
|
+
}
|
|
593
|
+
rows.push({ value: { kind: "action", action: "done" }, label: "Done" });
|
|
594
|
+
return {
|
|
595
|
+
title: `Plan-mode tools (${pageIndex + 1}/${pageCount}). Non-built-in tools run at user risk.`,
|
|
596
|
+
rows,
|
|
597
|
+
};
|
|
598
|
+
},
|
|
599
|
+
(value) => {
|
|
600
|
+
if (value.kind === "action") {
|
|
601
|
+
if (value.action === "done") return "close";
|
|
602
|
+
pageIndex += value.action === "previous" ? -1 : 1;
|
|
603
|
+
return "reset";
|
|
604
|
+
}
|
|
605
|
+
if (!canSelectToolInPlanMode(value.tool)) {
|
|
606
|
+
ctx.ui.notify(`${value.tool.name} is blocked in Plan mode.`, "warning");
|
|
607
|
+
return "stay";
|
|
608
|
+
}
|
|
609
|
+
togglePlanModeTool(value.tool, tools, ctx);
|
|
610
|
+
return "stay";
|
|
611
|
+
},
|
|
612
|
+
);
|
|
613
|
+
if (!customHandled) await showDialogToolSelector(ctx);
|
|
614
|
+
|
|
615
|
+
applyPlanModeTools();
|
|
616
|
+
persistState();
|
|
617
|
+
updateUi(ctx);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
async function showDialogToolSelector(ctx: ExtensionContext) {
|
|
550
621
|
let pageIndex = 0;
|
|
551
622
|
while (true) {
|
|
552
623
|
const tools = selectableTools();
|
|
@@ -570,7 +641,7 @@ export default function planMode(pi: ExtensionAPI) {
|
|
|
570
641
|
`Plan-mode tools (${pageIndex + 1}/${pageCount}). Non-built-in tools run at user risk.`,
|
|
571
642
|
[...choices, ...navigationChoices],
|
|
572
643
|
);
|
|
573
|
-
if (!choice || choice === doneChoice)
|
|
644
|
+
if (!choice || choice === doneChoice) return;
|
|
574
645
|
if (choice === previousChoice) {
|
|
575
646
|
pageIndex = Math.max(0, pageIndex - 1);
|
|
576
647
|
continue;
|
|
@@ -579,28 +650,24 @@ export default function planMode(pi: ExtensionAPI) {
|
|
|
579
650
|
pageIndex = Math.min(pageCount - 1, pageIndex + 1);
|
|
580
651
|
continue;
|
|
581
652
|
}
|
|
582
|
-
|
|
583
|
-
const selectedIndex = choices.indexOf(choice);
|
|
584
|
-
const tool = pageTools[selectedIndex];
|
|
653
|
+
const tool = pageTools[choices.indexOf(choice)];
|
|
585
654
|
if (!tool) continue;
|
|
586
655
|
if (!canSelectToolInPlanMode(tool)) {
|
|
587
656
|
ctx.ui.notify(`${tool.name} is blocked in Plan mode.`, "warning");
|
|
588
657
|
continue;
|
|
589
658
|
}
|
|
590
|
-
|
|
591
|
-
const nextSelectedNames = planModeSelectedNames(tools);
|
|
592
|
-
if (nextSelectedNames.has(tool.name)) nextSelectedNames.delete(tool.name);
|
|
593
|
-
else nextSelectedNames.add(tool.name);
|
|
594
|
-
|
|
595
|
-
state = {
|
|
596
|
-
...state,
|
|
597
|
-
selectedToolNames: filterAvailableSelectedNames(Array.from(nextSelectedNames), tools),
|
|
598
|
-
};
|
|
599
|
-
applyPlanModeTools();
|
|
600
|
-
persistState();
|
|
601
|
-
updateUi(ctx);
|
|
659
|
+
togglePlanModeTool(tool, tools, ctx);
|
|
602
660
|
}
|
|
661
|
+
}
|
|
603
662
|
|
|
663
|
+
function togglePlanModeTool(tool: ToolInfo, tools: ToolInfo[], ctx: ExtensionContext) {
|
|
664
|
+
const nextSelectedNames = planModeSelectedNames(tools);
|
|
665
|
+
if (nextSelectedNames.has(tool.name)) nextSelectedNames.delete(tool.name);
|
|
666
|
+
else nextSelectedNames.add(tool.name);
|
|
667
|
+
state = {
|
|
668
|
+
...state,
|
|
669
|
+
selectedToolNames: filterAvailableSelectedNames(Array.from(nextSelectedNames), tools),
|
|
670
|
+
};
|
|
604
671
|
applyPlanModeTools();
|
|
605
672
|
persistState();
|
|
606
673
|
updateUi(ctx);
|
|
@@ -697,7 +764,7 @@ export default function planMode(pi: ExtensionAPI) {
|
|
|
697
764
|
function applyPlanThinkingLevel() {
|
|
698
765
|
if (state.manualThinkingLevel) {
|
|
699
766
|
if (pi.getThinkingLevel() !== state.manualThinkingLevel) {
|
|
700
|
-
pi
|
|
767
|
+
setPlanThinkingLevel(pi, state.manualThinkingLevel);
|
|
701
768
|
}
|
|
702
769
|
return;
|
|
703
770
|
}
|
|
@@ -712,7 +779,7 @@ export default function planMode(pi: ExtensionAPI) {
|
|
|
712
779
|
}
|
|
713
780
|
const current = pi.getThinkingLevel();
|
|
714
781
|
if (!state.appliedThinkingLevel) state.previousThinkingLevel = current;
|
|
715
|
-
if (current !== configured) pi
|
|
782
|
+
if (current !== configured) setPlanThinkingLevel(pi, configured);
|
|
716
783
|
state.appliedThinkingLevel = pi.getThinkingLevel();
|
|
717
784
|
}
|
|
718
785
|
|
|
@@ -736,7 +803,7 @@ export default function planMode(pi: ExtensionAPI) {
|
|
|
736
803
|
previousThinkingLevel &&
|
|
737
804
|
pi.getThinkingLevel() === appliedThinkingLevel
|
|
738
805
|
) {
|
|
739
|
-
pi
|
|
806
|
+
setPlanThinkingLevel(pi, previousThinkingLevel);
|
|
740
807
|
}
|
|
741
808
|
state = { ...state, appliedThinkingLevel: undefined, previousThinkingLevel: undefined };
|
|
742
809
|
}
|
|
@@ -806,11 +873,6 @@ export default function planMode(pi: ExtensionAPI) {
|
|
|
806
873
|
return `Tools: ${names.length > 0 ? names.join(", ") : "none"}`;
|
|
807
874
|
}
|
|
808
875
|
|
|
809
|
-
function isBuiltinToolName(toolName: string) {
|
|
810
|
-
const tool = toolByName(toolName);
|
|
811
|
-
return tool ? isBuiltinTool(tool) : toolName === "bash";
|
|
812
|
-
}
|
|
813
|
-
|
|
814
876
|
function toolByName(toolName: string) {
|
|
815
877
|
return safeGetAllTools().find((candidate) => candidate.name === toolName);
|
|
816
878
|
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
export interface PersistentSelectorRow<T> {
|
|
4
|
+
value: T;
|
|
5
|
+
label: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface PersistentSelectorView<T> {
|
|
9
|
+
title: string;
|
|
10
|
+
rows: PersistentSelectorRow<T>[];
|
|
11
|
+
help?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type PersistentSelectorActivation = "stay" | "reset" | "close";
|
|
15
|
+
|
|
16
|
+
export async function showPersistentSelector<T>(
|
|
17
|
+
ctx: ExtensionContext,
|
|
18
|
+
getView: () => PersistentSelectorView<T>,
|
|
19
|
+
onActivate: (value: T) => PersistentSelectorActivation,
|
|
20
|
+
): Promise<boolean> {
|
|
21
|
+
const result = await ctx.ui.custom<"closed" | undefined>((tui, theme, keybindings, done) => {
|
|
22
|
+
let selectedIndex = 0;
|
|
23
|
+
const currentView = () => {
|
|
24
|
+
const view = getView();
|
|
25
|
+
selectedIndex = Math.min(selectedIndex, Math.max(0, view.rows.length - 1));
|
|
26
|
+
return view;
|
|
27
|
+
};
|
|
28
|
+
const moveSelection = (delta: number) => {
|
|
29
|
+
const { rows } = currentView();
|
|
30
|
+
if (rows.length === 0) return;
|
|
31
|
+
selectedIndex = (selectedIndex + delta + rows.length) % rows.length;
|
|
32
|
+
};
|
|
33
|
+
const activateSelectedRow = () => {
|
|
34
|
+
const row = currentView().rows[selectedIndex];
|
|
35
|
+
if (!row) return;
|
|
36
|
+
const action = onActivate(row.value);
|
|
37
|
+
if (action === "reset") selectedIndex = 0;
|
|
38
|
+
else if (action === "close") done("closed");
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
invalidate() {},
|
|
43
|
+
render(width: number) {
|
|
44
|
+
const view = currentView();
|
|
45
|
+
return [
|
|
46
|
+
theme.fg("accent", theme.bold(clipLine(view.title, width))),
|
|
47
|
+
"",
|
|
48
|
+
...view.rows.map((row, index) => {
|
|
49
|
+
const prefix = index === selectedIndex ? "› " : " ";
|
|
50
|
+
const line = clipLine(`${prefix}${row.label}`, width);
|
|
51
|
+
return index === selectedIndex ? theme.fg("accent", line) : line;
|
|
52
|
+
}),
|
|
53
|
+
"",
|
|
54
|
+
theme.fg(
|
|
55
|
+
"dim",
|
|
56
|
+
clipLine(view.help ?? "↑↓ navigate • Enter/Space toggle • Esc close", width),
|
|
57
|
+
),
|
|
58
|
+
];
|
|
59
|
+
},
|
|
60
|
+
handleInput(data: string) {
|
|
61
|
+
if (keybindings.matches(data, "tui.select.up")) {
|
|
62
|
+
moveSelection(-1);
|
|
63
|
+
tui.requestRender();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (keybindings.matches(data, "tui.select.down")) {
|
|
67
|
+
moveSelection(1);
|
|
68
|
+
tui.requestRender();
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (keybindings.matches(data, "tui.select.pageUp")) {
|
|
72
|
+
selectedIndex = 0;
|
|
73
|
+
tui.requestRender();
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (keybindings.matches(data, "tui.select.pageDown")) {
|
|
77
|
+
selectedIndex = Math.max(0, currentView().rows.length - 1);
|
|
78
|
+
tui.requestRender();
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (keybindings.matches(data, "tui.select.confirm") || data === " ") {
|
|
82
|
+
activateSelectedRow();
|
|
83
|
+
tui.requestRender();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (keybindings.matches(data, "tui.select.cancel")) done("closed");
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
});
|
|
90
|
+
return result === "closed";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function clipLine(value: string, width: number) {
|
|
94
|
+
return Array.from(value).slice(0, Math.max(0, width)).join("");
|
|
95
|
+
}
|