@pi-unipi/ask-user 0.1.10 → 0.1.11

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 CHANGED
@@ -1,30 +1,24 @@
1
1
  # @pi-unipi/ask-user
2
2
 
3
- Structured user input tool for the Pi coding agent — part of the Unipi suite.
3
+ Structured user input for decision gates. When the agent needs you to pick between options which database, which approach, which files to change — it calls `ask_user` instead of guessing.
4
4
 
5
- ## Features
5
+ Three input modes: single-select (pick one), multi-select (toggle several), freeform (type your own). The agent presents the question, you answer, it continues.
6
6
 
7
- ### `ask_user` Tool
7
+ ## Commands
8
8
 
9
- Ask the user a question with structured options. Supports three modes:
9
+ Ask-user has no user commands. It's an agent tool package the agent calls it when it needs input.
10
10
 
11
- - **Single-select** — Pick one option from a list
12
- - **Multi-select** — Toggle multiple options, then submit
13
- - **Freeform** — Type a custom answer
11
+ ## Special Triggers
14
12
 
15
- ### Usage
13
+ All workflow skills detect ask-user and use it for decision gates. Instead of the agent deciding on its own, it presents options and waits for your input. This happens naturally during brainstorm, plan, work, and other skills when the agent faces ambiguity.
16
14
 
17
- The agent calls the tool when it needs user input:
15
+ The bundled skill guides the agent to use `ask_user` for high-stakes decisions — architecture choices, database selection, naming decisions, anything with lasting impact.
18
16
 
19
- ```typescript
20
- ask_user({
21
- question: "Which database should we use?",
22
- options: [
23
- { label: "PostgreSQL", description: "Reliable, feature-rich" },
24
- { label: "SQLite", description: "Simple, serverless" },
25
- ],
26
- })
27
- ```
17
+ ## Agent Tool
18
+
19
+ | Tool | Description |
20
+ |------|-------------|
21
+ | `ask_user` | Structured user input with options |
28
22
 
29
23
  ### Parameters
30
24
 
@@ -37,12 +31,24 @@ ask_user({
37
31
  | `allowFreeform` | boolean? | true | Allow freeform text input |
38
32
  | `timeout` | number? | — | Auto-dismiss after N ms |
39
33
 
34
+ ### Example
35
+
36
+ ```typescript
37
+ ask_user({
38
+ question: "Which database should we use?",
39
+ options: [
40
+ { label: "PostgreSQL", description: "Reliable, feature-rich" },
41
+ { label: "SQLite", description: "Simple, serverless" },
42
+ ],
43
+ })
44
+ ```
45
+
40
46
  ### Keyboard Controls
41
47
 
42
48
  | Mode | Keys |
43
49
  |------|------|
44
- | Single-select | ↑↓ navigate, Enter select, Esc cancel |
45
- | Multi-select | ↑↓ navigate, Space toggle, Enter submit, Esc cancel |
50
+ | Single-select | Up/Down navigate, Enter select, Esc cancel |
51
+ | Multi-select | Up/Down navigate, Space toggle, Enter submit, Esc cancel |
46
52
  | Freeform | Type text, Enter submit, Esc back |
47
53
 
48
54
  ### TUI Display
@@ -57,7 +63,7 @@ ask_user({
57
63
  Option C
58
64
  Type something...
59
65
 
60
- ↑↓ navigate Enter select Esc cancel
66
+ Up/Down navigate, Enter select, Esc cancel
61
67
  ─────────────────────────────
62
68
  ```
63
69
 
@@ -66,34 +72,19 @@ ask_user({
66
72
  ─────────────────────────────
67
73
  Which features to enable?
68
74
  ─────────────────────────────
69
- > [] Logging
75
+ > [x] Logging
70
76
  [ ] Metrics
71
- [] Tracing
77
+ [x] Tracing
72
78
  [ ] Type something...
73
79
 
74
- ↑↓ navigate Space toggle Enter submit Esc cancel
80
+ Up/Down navigate, Space toggle, Enter submit, Esc cancel
75
81
  ─────────────────────────────
76
82
  ```
77
83
 
78
- ## Installation
79
-
80
- ```bash
81
- pi install npm:@pi-unipi/ask-user
82
- ```
83
-
84
- Or install the full Unipi suite:
85
-
86
- ```bash
87
- pi install npm:@pi-unipi/unipi
88
- ```
89
-
90
- ## Bundled Skill
84
+ ## Configurables
91
85
 
92
- The package includes a skill that guides the agent to use `ask_user` for high-stakes decisions. The skill is automatically discovered when the extension loads.
86
+ Ask-user has no configuration. Input mode is determined by the `allowMultiple` and `allowFreeform` parameters the agent passes.
93
87
 
94
- ## Dependencies
88
+ ## License
95
89
 
96
- - `@pi-unipi/core` — Shared constants and utilities
97
- - `@mariozechner/pi-coding-agent` — Pi extension API
98
- - `@mariozechner/pi-tui` — TUI components
99
- - `@sinclair/typebox` — Schema validation
90
+ MIT
package/ask-ui.ts CHANGED
@@ -638,7 +638,24 @@ export function createRenderResult() {
638
638
  0,
639
639
  0,
640
640
  );
641
- case "new_session":
641
+ case "new_session": {
642
+ const launchedWith = (response as any).launchedWith;
643
+ if (launchedWith === "compact") {
644
+ return new Text(
645
+ theme.fg("success", "✓ compacted → ") +
646
+ theme.fg("accent", response.prefill || ""),
647
+ 0,
648
+ 0,
649
+ );
650
+ }
651
+ if (launchedWith === "direct") {
652
+ return new Text(
653
+ theme.fg("success", "✓ running → ") +
654
+ theme.fg("accent", response.prefill || ""),
655
+ 0,
656
+ 0,
657
+ );
658
+ }
642
659
  return new Text(
643
660
  theme.fg("success", "✓ ") +
644
661
  theme.fg("muted", "new session") +
@@ -646,6 +663,7 @@ export function createRenderResult() {
646
663
  0,
647
664
  0,
648
665
  );
666
+ }
649
667
  default:
650
668
  return new Text(
651
669
  theme.fg("text", JSON.stringify(response)),
package/launcher-ui.ts ADDED
@@ -0,0 +1,142 @@
1
+ /**
2
+ * @pi-unipi/ask-user — Session Launcher TUI
3
+ *
4
+ * Secondary overlay shown when user selects a new_session option.
5
+ * Offers Compact & run, Run directly, or Cancel.
6
+ */
7
+
8
+ import { Key, matchesKey, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
9
+ import type { SessionLauncherResult } from "./types.js";
10
+
11
+ /** Launcher option definition */
12
+ interface LauncherOption {
13
+ label: string;
14
+ icon: string;
15
+ action: SessionLauncherResult["action"];
16
+ }
17
+
18
+ const OPTIONS: LauncherOption[] = [
19
+ { label: "Compact & run", icon: "🧹", action: "compact" },
20
+ { label: "Run directly", icon: "▶", action: "direct" },
21
+ { label: "Cancel", icon: "✕", action: "cancel" },
22
+ ];
23
+
24
+ /**
25
+ * Render the session launcher UI.
26
+ *
27
+ * Simple single-select picker with 3 fixed options.
28
+ * No editor, no timeout, no multi-select.
29
+ */
30
+ export function renderLauncherUI(params: {
31
+ prefill: string;
32
+ }): (
33
+ tui: any,
34
+ theme: any,
35
+ kb: any,
36
+ done: (result: SessionLauncherResult | null) => void,
37
+ ) => {
38
+ render: (width: number) => string[];
39
+ invalidate: () => void;
40
+ handleInput: (data: string) => void;
41
+ } {
42
+ return (_tui, theme, _kb, done) => {
43
+ const { prefill } = params;
44
+
45
+ // State
46
+ let optionIndex = 0;
47
+ let cachedLines: string[] | undefined;
48
+
49
+ function refresh() {
50
+ cachedLines = undefined;
51
+ _tui.requestRender();
52
+ }
53
+
54
+ function handleInput(data: string) {
55
+ // Navigation
56
+ if (matchesKey(data, Key.up)) {
57
+ optionIndex = Math.max(0, optionIndex - 1);
58
+ refresh();
59
+ return;
60
+ }
61
+ if (matchesKey(data, Key.down)) {
62
+ optionIndex = Math.min(OPTIONS.length - 1, optionIndex + 1);
63
+ refresh();
64
+ return;
65
+ }
66
+
67
+ // Enter: select
68
+ if (matchesKey(data, Key.enter)) {
69
+ const opt = OPTIONS[optionIndex];
70
+ done({ action: opt.action, prefill });
71
+ return;
72
+ }
73
+
74
+ // Escape: cancel
75
+ if (matchesKey(data, Key.escape)) {
76
+ done(null);
77
+ return;
78
+ }
79
+ }
80
+
81
+ function render(width: number): string[] {
82
+ if (cachedLines) return cachedLines;
83
+
84
+ const lines: string[] = [];
85
+ const innerWidth = Math.max(40, width - 2);
86
+ const border = (s: string) => theme.fg("accent", s);
87
+
88
+ function padVisible(content: string, targetWidth: number): string {
89
+ const vw = visibleWidth(content);
90
+ const pad = Math.max(0, targetWidth - vw);
91
+ return content + " ".repeat(pad);
92
+ }
93
+
94
+ const add = (s: string) =>
95
+ lines.push(
96
+ border("│") +
97
+ padVisible(truncateToWidth(s, innerWidth), innerWidth) +
98
+ border("│"),
99
+ );
100
+ const addEmpty = () =>
101
+ lines.push(border("│") + " ".repeat(innerWidth) + border("│"));
102
+
103
+ // Top border
104
+ lines.push(border(`╭${"─".repeat(innerWidth)}╮`));
105
+
106
+ // Header: show prefill command (truncated)
107
+ const headerPrefix = " 🚀 ";
108
+ const maxPrefillWidth = innerWidth - headerPrefix.length - 1;
109
+ const truncatedPrefill = truncateToWidth(prefill || "(no command)", maxPrefillWidth);
110
+ add(theme.fg("accent", headerPrefix) + theme.fg("text", truncatedPrefill));
111
+ addEmpty();
112
+
113
+ // Options
114
+ for (let i = 0; i < OPTIONS.length; i++) {
115
+ const opt = OPTIONS[i];
116
+ const isSelected = i === optionIndex;
117
+ const prefix = isSelected ? theme.fg("accent", "> ") : " ";
118
+ const label = `${opt.icon} ${opt.label}`;
119
+ const color = isSelected ? "accent" : "text";
120
+ add(prefix + theme.fg(color, label));
121
+ }
122
+
123
+ // Footer hint
124
+ addEmpty();
125
+ add(theme.fg("dim", " ↑↓ navigate • Enter select • Esc cancel"));
126
+
127
+ // Bottom border
128
+ lines.push(border(`╰${"─".repeat(innerWidth)}╯`));
129
+
130
+ cachedLines = lines;
131
+ return lines;
132
+ }
133
+
134
+ return {
135
+ render,
136
+ invalidate: () => {
137
+ cachedLines = undefined;
138
+ },
139
+ handleInput,
140
+ };
141
+ };
142
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/ask-user",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "description": "Structured user input tool for Pi coding agent — single-select, multi-select, freeform",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -24,6 +24,7 @@
24
24
  "types.ts",
25
25
  "tools.ts",
26
26
  "ask-ui.ts",
27
+ "launcher-ui.ts",
27
28
  "commands.ts",
28
29
  "config.ts",
29
30
  "settings-tui.ts",
@@ -56,7 +56,7 @@ Use the `ask_user` tool to collect structured input from the user.
56
56
  | `"select"` | Normal selection (default). Returns immediately. |
57
57
  | `"input"` | Enters text input mode. Returns `combined` response with selection + text. |
58
58
  | `"end_turn"` | Signals end of agent turn. Returns `end_turn` response kind. |
59
- | `"new_session"` | Starts a new session. Returns `new_session` response kind with optional `prefill`. |
59
+ | `"new_session"` | Starts a new session. Returns `new_session` response kind with optional `prefill`. Shows a launcher overlay offering **Compact & run** (compacts context first) or **Run directly**. |
60
60
 
61
61
  ## Examples
62
62
 
@@ -150,3 +150,15 @@ ask_user({
150
150
  - "I want changes" enters text input mode for the user to explain
151
151
  - "Done for now" signals the agent to end its turn
152
152
  - "Start fresh" starts a new session with the prefill message
153
+
154
+ ## Session Launcher
155
+
156
+ When a user selects a `new_session` option, a secondary launcher overlay appears with three choices:
157
+
158
+ | Choice | Behavior |
159
+ |--------|----------|
160
+ | 🧹 Compact & run | Compacts current context (via `ctx.compact()`), then returns the prefill command to the LLM |
161
+ | ▶ Run directly | Returns the prefill command to the LLM without compaction |
162
+ | ✕ Cancel | Cancels the session launch |
163
+
164
+ This two-step flow lets the user manage context window usage before starting a new task.
package/tools.ts CHANGED
@@ -11,8 +11,9 @@ import {
11
11
  UNIPI_EVENTS,
12
12
  emitEvent,
13
13
  } from "@pi-unipi/core";
14
- import type { NormalizedOption, AskUserResponse } from "./types.js";
14
+ import type { NormalizedOption, AskUserResponse, SessionLauncherResult } from "./types.js";
15
15
  import { renderAskUI, createRenderCall, createRenderResult } from "./ask-ui.js";
16
+ import { renderLauncherUI } from "./launcher-ui.js";
16
17
  import { getAskUserSettings } from "./config.js";
17
18
 
18
19
  /**
@@ -331,6 +332,57 @@ export function registerAskUserTools(pi: ExtensionAPI): void {
331
332
  contentText = "No response";
332
333
  }
333
334
 
335
+ // Session launcher intercept: when user selects new_session, offer compact/direct/cancel
336
+ if (response.kind === "new_session") {
337
+ const prefill = response.prefill || "";
338
+ const launcherResult = await ctx.ui.custom<SessionLauncherResult | null>(
339
+ renderLauncherUI({ prefill }),
340
+ );
341
+
342
+ if (!launcherResult || launcherResult.action === "cancel") {
343
+ return {
344
+ content: [{ type: "text", text: "User cancelled the session launch" }],
345
+ details: {
346
+ question,
347
+ options: normalizedOptions.map((o) => o.label),
348
+ response: {
349
+ kind: "cancelled",
350
+ comment: "Session launcher cancelled",
351
+ } as AskUserResponse,
352
+ },
353
+ };
354
+ }
355
+
356
+ if (launcherResult.action === "compact") {
357
+ try {
358
+ await new Promise<void>((resolve, reject) => {
359
+ ctx.compact({
360
+ customInstructions: `Preparing for new task. Summarize previous work concisely, preserving only what's essential for: ${prefill}`,
361
+ onComplete: () => resolve(),
362
+ onError: (err) => reject(err),
363
+ });
364
+ });
365
+ } catch (err) {
366
+ // Compaction failure shouldn't block the session launch — continue anyway
367
+ }
368
+ }
369
+
370
+ const actionLabel = launcherResult.action === "compact" ? "compacted" : "running";
371
+ contentText = `User chose to proceed (${actionLabel}): ${prefill}`;
372
+
373
+ return {
374
+ content: [{ type: "text", text: contentText }],
375
+ details: {
376
+ question,
377
+ options: normalizedOptions.map((o) => o.label),
378
+ response: {
379
+ ...response,
380
+ launchedWith: launcherResult.action,
381
+ },
382
+ },
383
+ };
384
+ }
385
+
334
386
  return {
335
387
  content: [{ type: "text", text: contentText }],
336
388
  details: {
package/types.ts CHANGED
@@ -54,6 +54,12 @@ export interface AskUserResponse {
54
54
  comment?: string;
55
55
  }
56
56
 
57
+ /** Result from the session launcher UI */
58
+ export interface SessionLauncherResult {
59
+ action: "compact" | "direct" | "cancel";
60
+ prefill: string;
61
+ }
62
+
57
63
  /** Normalized option with resolved value */
58
64
  export interface NormalizedOption {
59
65
  label: string;