@pi-unipi/unipi 0.1.17 โ†’ 2.0.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.
Files changed (118) hide show
  1. package/README.md +18 -18
  2. package/package.json +7 -4
  3. package/packages/ask-user/README.md +30 -0
  4. package/packages/ask-user/ask-ui.ts +54 -14
  5. package/packages/ask-user/commands.ts +1 -1
  6. package/packages/ask-user/handoff.ts +163 -0
  7. package/packages/ask-user/launcher-ui.ts +143 -0
  8. package/packages/ask-user/skills/ask-user/SKILL.md +14 -2
  9. package/packages/ask-user/tools.ts +92 -5
  10. package/packages/ask-user/types.ts +28 -0
  11. package/packages/autocomplete/src/__tests__/provider.sorting.test.ts +423 -0
  12. package/packages/autocomplete/src/constants.ts +28 -14
  13. package/packages/autocomplete/src/provider.ts +16 -3
  14. package/packages/autocomplete/src/sorting.ts +81 -0
  15. package/packages/cocoindex/README.md +93 -0
  16. package/packages/cocoindex/bridge.ts +774 -0
  17. package/packages/cocoindex/commands.ts +175 -0
  18. package/packages/cocoindex/index.ts +55 -0
  19. package/packages/cocoindex/installer.ts +397 -0
  20. package/packages/cocoindex/skills/cocoindex/SKILL.md +88 -0
  21. package/packages/cocoindex/tools.ts +131 -0
  22. package/packages/compactor/README.md +3 -1
  23. package/packages/compactor/src/commands/index.ts +79 -169
  24. package/packages/compactor/src/compaction/content.ts +2 -2
  25. package/packages/compactor/src/compaction/cut.ts +10 -6
  26. package/packages/compactor/src/compaction/hooks.ts +82 -52
  27. package/packages/compactor/src/compaction/recall-scope.ts +1 -1
  28. package/packages/compactor/src/config/manager.ts +0 -0
  29. package/packages/compactor/src/config/presets.ts +10 -10
  30. package/packages/compactor/src/executor/executor.ts +4 -4
  31. package/packages/compactor/src/index.ts +56 -48
  32. package/packages/compactor/src/info-screen.ts +97 -40
  33. package/packages/compactor/src/session/analytics.ts +8 -1
  34. package/packages/compactor/src/session/db.ts +40 -11
  35. package/packages/compactor/src/session/extract.ts +37 -0
  36. package/packages/compactor/src/tools/ctx-batch-execute.ts +5 -16
  37. package/packages/compactor/src/tools/ctx-doctor.ts +0 -18
  38. package/packages/compactor/src/tools/ctx-stats.ts +69 -12
  39. package/packages/compactor/src/tools/register.ts +30 -122
  40. package/packages/compactor/src/tui/settings-overlay.ts +12 -21
  41. package/packages/compactor/src/types.ts +8 -26
  42. package/packages/core/constants.ts +29 -7
  43. package/packages/core/events.ts +39 -3
  44. package/packages/core/global-types.ts +46 -0
  45. package/packages/core/global.d.ts +23 -0
  46. package/packages/core/sandbox.ts +61 -13
  47. package/packages/core/utils.ts +30 -0
  48. package/packages/footer/src/commands.ts +38 -123
  49. package/packages/footer/src/config.ts +16 -0
  50. package/packages/footer/src/help.ts +160 -0
  51. package/packages/footer/src/index.ts +48 -15
  52. package/packages/footer/src/presets.ts +41 -32
  53. package/packages/footer/src/registry/index.ts +1 -1
  54. package/packages/footer/src/rendering/icons.ts +77 -59
  55. package/packages/footer/src/rendering/renderer.ts +246 -80
  56. package/packages/footer/src/rendering/theme.ts +56 -29
  57. package/packages/footer/src/segments/compactor.ts +76 -30
  58. package/packages/footer/src/segments/core.ts +124 -15
  59. package/packages/footer/src/segments/kanboard.ts +25 -9
  60. package/packages/footer/src/segments/mcp.ts +25 -16
  61. package/packages/footer/src/segments/memory.ts +9 -6
  62. package/packages/footer/src/segments/notify.ts +16 -5
  63. package/packages/footer/src/segments/ralph.ts +23 -8
  64. package/packages/footer/src/segments/status-ext.ts +1 -1
  65. package/packages/footer/src/segments/workflow.ts +41 -18
  66. package/packages/footer/src/tps-tracker.ts +204 -0
  67. package/packages/footer/src/tui/settings-tui.ts +253 -63
  68. package/packages/footer/src/types.ts +51 -12
  69. package/packages/info-screen/core-groups.ts +3 -2
  70. package/packages/info-screen/index.ts +10 -9
  71. package/packages/info-screen/registry.ts +3 -4
  72. package/packages/input-shortcuts/src/index.ts +2 -3
  73. package/packages/kanboard/commands.ts +5 -5
  74. package/packages/kanboard/index.ts +1 -2
  75. package/packages/kanboard/tui/kanboard-overlay.ts +10 -8
  76. package/packages/mcp/src/index.ts +7 -8
  77. package/packages/mcp/src/tui/add-overlay.ts +11 -9
  78. package/packages/mcp/src/tui/settings-overlay.ts +5 -4
  79. package/packages/memory/embedding.ts +7 -7
  80. package/packages/memory/index.ts +55 -22
  81. package/packages/memory/storage.ts +55 -30
  82. package/packages/memory/tools.ts +15 -5
  83. package/packages/milestone/commands.ts +3 -3
  84. package/packages/milestone/index.ts +2 -3
  85. package/packages/notify/commands.ts +5 -5
  86. package/packages/ralph/index.ts +1 -2
  87. package/packages/subagents/src/index.ts +5 -5
  88. package/packages/subagents/src/widget.ts +4 -3
  89. package/packages/unipi/index.ts +2 -0
  90. package/packages/updater/src/checker.ts +6 -6
  91. package/packages/updater/src/commands.ts +6 -6
  92. package/packages/updater/src/index.ts +12 -10
  93. package/packages/updater/src/installer.ts +9 -9
  94. package/packages/updater/src/readme.ts +7 -8
  95. package/packages/updater/src/tui/changelog-overlay.ts +6 -5
  96. package/packages/updater/src/tui/readme-overlay.ts +4 -4
  97. package/packages/updater/src/tui/settings-overlay.ts +3 -3
  98. package/packages/updater/src/tui/update-overlay.ts +3 -3
  99. package/packages/utility/src/commands.ts +2 -2
  100. package/packages/utility/src/diff/highlighter.ts +1 -1
  101. package/packages/utility/src/diff/theme.ts +0 -0
  102. package/packages/utility/src/index.ts +21 -18
  103. package/packages/web-api/src/engine/dependencies.ts +4 -4
  104. package/packages/web-api/src/index.ts +1 -2
  105. package/packages/workflow/commands.ts +1 -1
  106. package/packages/workflow/index.ts +15 -7
  107. package/packages/workflow/skills/brainstorm/SKILL.md +20 -6
  108. package/packages/workflow/skills/debug/SKILL.md +4 -2
  109. package/packages/workflow/skills/plan/SKILL.md +36 -13
  110. package/packages/workflow/skills/review-work/SKILL.md +21 -1
  111. package/packages/workflow/skills/work/SKILL.md +22 -1
  112. package/packages/compactor/src/store/chunking.ts +0 -126
  113. package/packages/compactor/src/store/db-base.ts +0 -87
  114. package/packages/compactor/src/store/index.ts +0 -513
  115. package/packages/compactor/src/store/unified.ts +0 -109
  116. package/packages/compactor/src/tools/ctx-fetch-and-index.ts +0 -32
  117. package/packages/compactor/src/tools/ctx-index.ts +0 -36
  118. package/packages/compactor/src/tools/ctx-search.ts +0 -19
package/README.md CHANGED
@@ -9,39 +9,39 @@ pi install npm:@pi-unipi/unipi
9
9
 
10
10
  ## What You Get
11
11
 
12
- **Workflow** โ€” 20 commands that take ideas to shipped code. Brainstorm, plan, execute in worktrees, review, consolidate. The agent follows skill files step by step.
12
+ **[Workflow](./packages/workflow/README.md)** โ€” 20 commands that take ideas to shipped code. Brainstorm, plan, execute in worktrees, review, consolidate. The agent follows skill files step by step.
13
13
 
14
- **Ralph** โ€” Long-running loops that persist across sessions. Start a task, iterate through checklist items, resume after crashes. Progress tracked, state saved.
14
+ **[Ralph](./packages/ralph/README.md)** โ€” Long-running loops that persist across sessions. Start a task, iterate through checklist items, resume after crashes. Progress tracked, state saved.
15
15
 
16
- **Memory** โ€” SQLite + vector search stores facts, preferences, and decisions. Project-scoped and global. The agent remembers what you told it last week.
16
+ **[Memory](./packages/memory/README.md)** โ€” SQLite + vector search stores facts, preferences, and decisions. Project-scoped and global. The agent remembers what you told it last week.
17
17
 
18
- **Compactor** โ€” Zero-LLM context engine. 6-stage pipeline hits 95%+ token reduction at zero API cost. Session continuity, sandbox execution, FTS5 search.
18
+ **[Compactor](./packages/compactor/README.md)** โ€” Zero-LLM context engine. 6-stage pipeline hits 95%+ token reduction at zero API cost. Session continuity, sandbox execution, FTS5 search.
19
19
 
20
- **Subagents** โ€” Parallel execution with file locking. Spawn background agents to research, fix, or build while the main agent keeps going.
20
+ **[Subagents](./packages/subagents/README.md)** โ€” Parallel execution with file locking. Spawn background agents to research, fix, or build while the main agent keeps going.
21
21
 
22
- **Web API** โ€” Web search, page reading, content summarization. Smart-fetch engine with browser-grade TLS fingerprinting โ€” free, no API key. Paid providers as fallbacks.
22
+ **[Web API](./packages/web-api/README.md)** โ€” Web search, page reading, content summarization. Smart-fetch engine with browser-grade TLS fingerprinting โ€” free, no API key. Paid providers as fallbacks.
23
23
 
24
- **MCP** โ€” Browse 7,800+ MCP servers, add them interactively. Tools from servers register automatically as Pi tools.
24
+ **[MCP](./packages/mcp/README.md)** โ€” Browse 7,800+ MCP servers, add them interactively. Tools from servers register automatically as Pi tools.
25
25
 
26
- **Notify** โ€” Push notifications to native OS, Gotify, Telegram, or ntfy. Per-event platform routing. Configure once, get alerts everywhere.
26
+ **[Notify](./packages/notify/README.md)** โ€” Push notifications to native OS, Gotify, Telegram, or ntfy. Per-event platform routing. Configure once, get alerts everywhere.
27
27
 
28
- **Footer** โ€” Persistent status bar showing live stats from every package. Responsive layout, presets, per-segment toggling.
28
+ **[Footer](./packages/footer/README.md)** โ€” Persistent status bar showing live stats from every package. Responsive layout, presets, per-segment toggling.
29
29
 
30
- **BTW** โ€” Side conversations that run in parallel. Ask questions without interrupting the main agent.
30
+ **[BTW](./packages/btw/README.md)** โ€” Side conversations that run in parallel. Ask questions without interrupting the main agent.
31
31
 
32
- **Ask User** โ€” Structured input for decision gates. Single-select, multi-select, freeform. The agent asks instead of guessing.
32
+ **[Ask User](./packages/ask-user/README.md)** โ€” Structured input for decision gates. Single-select, multi-select, freeform. The agent asks instead of guessing.
33
33
 
34
- **Milestone** โ€” Track project goals across workflow cycles. MILESTONES.md stays in sync with specs, plans, and completed work.
34
+ **[Milestone](./packages/milestone/README.md)** โ€” Track project goals across workflow cycles. MILESTONES.md stays in sync with specs, plans, and completed work.
35
35
 
36
- **Kanboard** โ€” Web UI and TUI overlay for kanban boards. Parses all workflow documents into cards with progress indicators.
36
+ **[Kanboard](./packages/kanboard/README.md)** โ€” Web UI and TUI overlay for kanban boards. Parses all workflow documents into cards with progress indicators.
37
37
 
38
- **Info Screen** โ€” Dashboard overlay showing module status, tools, and custom data groups.
38
+ **[Info Screen](./packages/info-screen/README.md)** โ€” Dashboard overlay showing module status, tools, and custom data groups.
39
39
 
40
- **Utility** โ€” Environment info, diagnostics, cleanup, name badge, and Shiki-powered diff rendering.
40
+ **[Utility](./packages/utility/README.md)** โ€” Environment info, diagnostics, cleanup, name badge, and Shiki-powered diff rendering.
41
41
 
42
- **Updater** โ€” Checks npm for new versions on session start. Changelog browser and readme browser in TUI overlays.
42
+ **[Updater](./packages/updater/README.md)** โ€” Checks npm for new versions on session start. Changelog browser and readme browser in TUI overlays.
43
43
 
44
- **Input Shortcuts** โ€” Keyboard shortcuts via vim-style chord overlay. Stash/restore, undo/redo, clipboard, thinking toggle.
44
+ **[Input Shortcuts](./packages/input-shortcuts/README.md)** โ€” Keyboard shortcuts via vim-style chord overlay. Stash/restore, undo/redo, clipboard, thinking toggle.
45
45
 
46
46
  ## Architecture
47
47
 
@@ -77,7 +77,7 @@ Coexists triggers enhance behavior when packages are installed together. Workflo
77
77
  | Workflow | `/unipi:` | brainstorm, plan, work, review-work, consolidate, quick-work, debug, fix |
78
78
  | Ralph | `/unipi:ralph` | start, stop, resume, status |
79
79
  | Memory | `/unipi:memory-` | process, search, consolidate, forget |
80
- | Compactor | `/unipi:compact` | compact, stats, settings, preset |
80
+ | Compactor | `/unipi:` | lossless-compact, compact-stats, compact-settings, compact-preset, session-recall, content-index, content-search |
81
81
  | Notify | `/unipi:notify-` | settings, test, set-tg, set-ntfy |
82
82
  | MCP | `/unipi:mcp-` | add, settings, sync, status |
83
83
  | Web | `/unipi:web-` | settings, cache-clear |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/unipi",
3
- "version": "0.1.17",
3
+ "version": "2.0.0",
4
4
  "description": "All-in-one extension suite for Pi coding agent",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -55,7 +55,8 @@
55
55
  "node_modules/@pi-unipi/command-enchantment/src/index.ts",
56
56
  "node_modules/@pi-unipi/footer/src/index.ts",
57
57
  "node_modules/@pi-unipi/updater/src/index.ts",
58
- "node_modules/@pi-unipi/input-shortcuts/src/index.ts"
58
+ "node_modules/@pi-unipi/input-shortcuts/src/index.ts",
59
+ "node_modules/@pi-unipi/cocoindex/index.ts"
59
60
  ],
60
61
  "skills": [
61
62
  "node_modules/@pi-unipi/workflow/skills",
@@ -69,7 +70,8 @@
69
70
  "node_modules/@pi-unipi/notify/skills",
70
71
  "node_modules/@pi-unipi/milestone/skills",
71
72
  "node_modules/@pi-unipi/kanboard/skills",
72
- "node_modules/@pi-unipi/updater/skills"
73
+ "node_modules/@pi-unipi/updater/skills",
74
+ "node_modules/@pi-unipi/cocoindex/skills"
73
75
  ]
74
76
  },
75
77
  "peerDependencies": {
@@ -97,7 +99,8 @@
97
99
  "@pi-unipi/workflow": "*",
98
100
  "@pi-unipi/footer": "*",
99
101
  "@pi-unipi/updater": "*",
100
- "@pi-unipi/input-shortcuts": "*"
102
+ "@pi-unipi/input-shortcuts": "*",
103
+ "@pi-unipi/cocoindex": "*"
101
104
  },
102
105
  "devDependencies": {
103
106
  "@types/node": "^25.6.0",
@@ -12,6 +12,8 @@ Ask-user has no user commands. It's an agent tool package โ€” the agent calls it
12
12
 
13
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.
14
14
 
15
+ For workflow handoffs, options can use `action: "new_session"` with a `prefill`. Selecting one opens a launcher where **Compact & run** queues the prefill after compaction (or a short fallback) and **Run directly** queues it immediately. If automatic queuing fails, the prefill is placed in the editor for you to submit manually.
16
+
15
17
  The bundled skill guides the agent to use `ask_user` for high-stakes decisions โ€” architecture choices, database selection, naming decisions, anything with lasting impact.
16
18
 
17
19
  ## Agent Tool
@@ -43,6 +45,34 @@ ask_user({
43
45
  })
44
46
  ```
45
47
 
48
+ ### `new_session` Handoffs
49
+
50
+ ```typescript
51
+ ask_user({
52
+ question: "Continue with implementation?",
53
+ options: [
54
+ {
55
+ label: "Proceed to work",
56
+ value: "work",
57
+ action: "new_session",
58
+ prefill: "/unipi:work specs:2026-05-06-feature-plan.md",
59
+ },
60
+ { label: "Done for now", value: "done", action: "end_turn" },
61
+ ],
62
+ allowFreeform: false,
63
+ })
64
+ ```
65
+
66
+ When the user chooses a `new_session` option:
67
+
68
+ | Launcher choice | Behavior |
69
+ |-----------------|----------|
70
+ | ๐Ÿงน Compact & run | Starts context compaction, returns immediately, then queues/submits the prefill as a follow-up message from the compaction callback or a short fallback timer |
71
+ | โ–ถ Run directly | Queues/submits the prefill immediately as a follow-up message |
72
+ | โœ• Cancel | Cancels the handoff; no message is queued |
73
+
74
+ The tool result is rendered as `queued compact โ†’ ...` or `queued direct โ†’ ...`. If automatic delivery fails, ask-user falls back to editor prefill and warns you to press Enter.
75
+
46
76
  ### Keyboard Controls
47
77
 
48
78
  | Mode | Keys |
@@ -5,7 +5,8 @@
5
5
  * Uses ctx.ui.custom() callback pattern following question.ts/questionnaire.ts.
6
6
  */
7
7
 
8
- import { Editor, type EditorTheme, Key, matchesKey, Text, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@mariozechner/pi-tui";
8
+ import { Editor, type EditorTheme, Key, matchesKey, Text, truncateToWidth, type TUI, visibleWidth, wrapTextWithAnsi } from "@mariozechner/pi-tui";
9
+ import type { Theme, AgentToolResult } from "@mariozechner/pi-coding-agent";
9
10
  import type { NormalizedOption, AskUserResponse } from "./types.js";
10
11
 
11
12
  /** Result returned by the ask UI */
@@ -31,9 +32,9 @@ export function renderAskUI(params: {
31
32
  allowFreeform: boolean;
32
33
  timeout?: number;
33
34
  }): (
34
- tui: any,
35
- theme: any,
36
- kb: any,
35
+ tui: TUI,
36
+ theme: Theme,
37
+ kb: import("@mariozechner/pi-coding-agent").KeybindingsManager,
37
38
  done: (result: AskUIResult | null) => void,
38
39
  ) => {
39
40
  render: (width: number) => string[];
@@ -446,7 +447,7 @@ export function renderAskUI(params: {
446
447
  function renderOptions(
447
448
  lines: string[],
448
449
  add: (s: string) => void,
449
- theme: any,
450
+ theme: Theme,
450
451
  width: number,
451
452
  ) {
452
453
  for (let i = 0; i < displayOptions.length; i++) {
@@ -561,8 +562,8 @@ export function renderAskUI(params: {
561
562
  * Create a renderCall function for the ask_user tool.
562
563
  */
563
564
  export function createRenderCall() {
564
- return (args: any, theme: any, _context: any) => {
565
- const question = args.question || "";
565
+ return (args: Record<string, unknown>, theme: Theme, _context: unknown) => {
566
+ const question = (args.question as string) || "";
566
567
  const options = Array.isArray(args.options) ? args.options : [];
567
568
  const mode = args.allowMultiple ? "multi-select" : "single-select";
568
569
  const count = options.length;
@@ -581,14 +582,15 @@ export function createRenderCall() {
581
582
  * Create a renderResult function for the ask_user tool.
582
583
  */
583
584
  export function createRenderResult() {
584
- return (result: any, _options: any, theme: any, _context: any) => {
585
- const details = result.details;
585
+ return (result: AgentToolResult<unknown>, _options: unknown, theme: Theme, _context: unknown) => {
586
+ const details = result.details as Record<string, unknown> | undefined;
586
587
  if (!details) {
587
- const text = result.content?.[0];
588
- return new Text(text?.type === "text" ? text.text : "", 0, 0);
588
+ const content = result.content as unknown as Array<Record<string, unknown>> | undefined;
589
+ const text = content?.[0];
590
+ return new Text(text?.type === "text" ? (text.text as string) : "", 0, 0);
589
591
  }
590
592
 
591
- const response = details.response as AskUserResponse;
593
+ const response = (details as { response: AskUserResponse }).response;
592
594
  if (!response) {
593
595
  return new Text(theme.fg("warning", "No response"), 0, 0);
594
596
  }
@@ -638,14 +640,52 @@ export function createRenderResult() {
638
640
  0,
639
641
  0,
640
642
  );
641
- case "new_session":
643
+ case "new_session": {
644
+ const prefill = response.prefill || "";
645
+ if (response.launchStatus === "editor_prefill") {
646
+ const label = response.launchedWith === "compact"
647
+ ? "โš  compact editor prefill โ†’ "
648
+ : "โš  direct editor prefill โ†’ ";
649
+ return new Text(
650
+ theme.fg("warning", label) + theme.fg("accent", prefill),
651
+ 0,
652
+ 0,
653
+ );
654
+ }
655
+ if (response.launchStatus === "failed") {
656
+ const label = response.launchedWith === "compact"
657
+ ? "handoff failed (compact) โ†’ "
658
+ : "handoff failed (direct) โ†’ ";
659
+ return new Text(
660
+ theme.fg("error", label) + theme.fg("accent", prefill),
661
+ 0,
662
+ 0,
663
+ );
664
+ }
665
+ if (response.launchedWith === "compact") {
666
+ return new Text(
667
+ theme.fg("success", "โœ“ queued compact โ†’ ") +
668
+ theme.fg("accent", prefill),
669
+ 0,
670
+ 0,
671
+ );
672
+ }
673
+ if (response.launchedWith === "direct") {
674
+ return new Text(
675
+ theme.fg("success", "โœ“ queued direct โ†’ ") +
676
+ theme.fg("accent", prefill),
677
+ 0,
678
+ 0,
679
+ );
680
+ }
642
681
  return new Text(
643
682
  theme.fg("success", "โœ“ ") +
644
683
  theme.fg("muted", "new session") +
645
- (response.prefill ? theme.fg("accent", `: ${response.prefill}`) : ""),
684
+ (prefill ? theme.fg("accent", `: ${prefill}`) : ""),
646
685
  0,
647
686
  0,
648
687
  );
688
+ }
649
689
  default:
650
690
  return new Text(
651
691
  theme.fg("text", JSON.stringify(response)),
@@ -23,7 +23,7 @@ export function registerAskUserCommands(pi: ExtensionAPI): void {
23
23
  }
24
24
 
25
25
  ctx.ui.custom(
26
- (tui: any, _theme: any, _keybindings: any, done: any) => {
26
+ (tui, _theme, _keybindings, done) => {
27
27
  const overlay = new AskUserSettingsOverlay();
28
28
  overlay.onClose = () => done(undefined);
29
29
  return {
@@ -0,0 +1,163 @@
1
+ /**
2
+ * @pi-unipi/ask-user โ€” new_session handoff helpers
3
+ *
4
+ * Queues launcher prefill messages without waiting for LLM follow-up.
5
+ */
6
+
7
+ import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
8
+ import type { SessionLaunchReason, SessionLaunchStatus } from "./types.js";
9
+
10
+ /** Compact handoff fallback timer. Keeps the launcher from stalling if callbacks wait for the tool turn to finish. */
11
+ export const COMPACT_HANDOFF_FALLBACK_MS = 1500;
12
+
13
+ export interface HandoffResult {
14
+ status: SessionLaunchStatus;
15
+ reason: SessionLaunchReason;
16
+ prefill?: string;
17
+ error?: string;
18
+ }
19
+
20
+ interface NormalizedPrefill {
21
+ ok: true;
22
+ prefill: string;
23
+ }
24
+
25
+ interface InvalidPrefill {
26
+ ok: false;
27
+ reason: "empty-prefill";
28
+ }
29
+
30
+ function normalizePrefill(prefill: string | undefined): NormalizedPrefill | InvalidPrefill {
31
+ const trimmed = (prefill ?? "").trim();
32
+ if (!trimmed) {
33
+ return { ok: false, reason: "empty-prefill" };
34
+ }
35
+ return { ok: true, prefill: trimmed };
36
+ }
37
+
38
+ function errorMessage(error: unknown): string {
39
+ return error instanceof Error ? error.message : String(error);
40
+ }
41
+
42
+ function notify(ctx: ExtensionContext, message: string, level: "info" | "warning" | "error"): void {
43
+ try {
44
+ ctx.ui.notify(message, level);
45
+ } catch {
46
+ // Notifications are best-effort only; never let status UI break delivery.
47
+ }
48
+ }
49
+
50
+ function fallbackToEditor(
51
+ ctx: ExtensionContext,
52
+ prefill: string,
53
+ reason: SessionLaunchReason,
54
+ error: unknown,
55
+ ): HandoffResult {
56
+ try {
57
+ ctx.ui.setEditorText(prefill);
58
+ notify(ctx, "Automatic handoff failed. The command was placed in the editor; press Enter to run it.", "warning");
59
+ return {
60
+ status: "editor_prefill",
61
+ reason,
62
+ prefill,
63
+ error: errorMessage(error),
64
+ };
65
+ } catch (editorError) {
66
+ notify(ctx, `Automatic handoff failed and editor fallback failed: ${errorMessage(editorError)}`, "error");
67
+ return {
68
+ status: "failed",
69
+ reason,
70
+ prefill,
71
+ error: `${errorMessage(error)}; editor fallback: ${errorMessage(editorError)}`,
72
+ };
73
+ }
74
+ }
75
+
76
+ function deliverFollowUpMessage(
77
+ pi: ExtensionAPI,
78
+ ctx: ExtensionContext,
79
+ prefill: string,
80
+ reason: SessionLaunchReason,
81
+ ): HandoffResult {
82
+ try {
83
+ pi.sendUserMessage(prefill, { deliverAs: "followUp" });
84
+ return { status: "queued", reason, prefill };
85
+ } catch (error) {
86
+ return fallbackToEditor(ctx, prefill, reason, error);
87
+ }
88
+ }
89
+
90
+ export function queueDirectHandoff(
91
+ pi: ExtensionAPI,
92
+ ctx: ExtensionContext,
93
+ prefill: string | undefined,
94
+ ): HandoffResult {
95
+ const normalized = normalizePrefill(prefill);
96
+ if (!normalized.ok) {
97
+ return { status: "cancelled", reason: normalized.reason };
98
+ }
99
+
100
+ return deliverFollowUpMessage(pi, ctx, normalized.prefill, "direct");
101
+ }
102
+
103
+ export function queueCompactHandoff(params: {
104
+ pi: ExtensionAPI;
105
+ ctx: ExtensionContext;
106
+ prefill: string | undefined;
107
+ customInstructions: string;
108
+ }): HandoffResult {
109
+ const { pi, ctx, customInstructions } = params;
110
+ const normalized = normalizePrefill(params.prefill);
111
+ if (!normalized.ok) {
112
+ return { status: "cancelled", reason: normalized.reason };
113
+ }
114
+
115
+ const { prefill } = normalized;
116
+ let delivered = false;
117
+ let fallbackTimer: ReturnType<typeof setTimeout> | undefined;
118
+
119
+ const clearFallbackTimer = () => {
120
+ if (fallbackTimer) {
121
+ clearTimeout(fallbackTimer);
122
+ fallbackTimer = undefined;
123
+ }
124
+ };
125
+
126
+ const deliverOnce = (reason: SessionLaunchReason): HandoffResult | undefined => {
127
+ if (delivered) {
128
+ return undefined;
129
+ }
130
+ delivered = true;
131
+ clearFallbackTimer();
132
+ if (reason === "fallback-timeout") {
133
+ notify(ctx, "Compaction is still running; queued the selected handoff command via fallback.", "info");
134
+ }
135
+ return deliverFollowUpMessage(pi, ctx, prefill, reason);
136
+ };
137
+
138
+ fallbackTimer = setTimeout(() => {
139
+ deliverOnce("fallback-timeout");
140
+ }, COMPACT_HANDOFF_FALLBACK_MS);
141
+
142
+ try {
143
+ ctx.compact({
144
+ customInstructions,
145
+ onComplete: () => {
146
+ deliverOnce("compacted");
147
+ },
148
+ onError: (error) => {
149
+ notify(ctx, `Compaction reported an error; continuing handoff anyway: ${errorMessage(error)}`, "warning");
150
+ deliverOnce("compaction-error");
151
+ },
152
+ });
153
+ } catch (error) {
154
+ notify(ctx, `Could not start compaction; running selected handoff directly: ${errorMessage(error)}`, "warning");
155
+ return deliverOnce("compact-start-failed") ?? {
156
+ status: "queued",
157
+ reason: "compact-start-failed",
158
+ prefill,
159
+ };
160
+ }
161
+
162
+ return { status: "scheduled", reason: "compact-started", prefill };
163
+ }
@@ -0,0 +1,143 @@
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, type TUI, visibleWidth } from "@mariozechner/pi-tui";
9
+ import type { Theme, KeybindingsManager } from "@mariozechner/pi-coding-agent";
10
+ import type { SessionLauncherResult } from "./types.js";
11
+
12
+ /** Launcher option definition */
13
+ interface LauncherOption {
14
+ label: string;
15
+ icon: string;
16
+ action: SessionLauncherResult["action"];
17
+ }
18
+
19
+ const OPTIONS: LauncherOption[] = [
20
+ { label: "Compact & run", icon: "๐Ÿงน", action: "compact" },
21
+ { label: "Run directly", icon: "โ–ถ", action: "direct" },
22
+ { label: "Cancel", icon: "โœ•", action: "cancel" },
23
+ ];
24
+
25
+ /**
26
+ * Render the session launcher UI.
27
+ *
28
+ * Simple single-select picker with 3 fixed options.
29
+ * No editor, no timeout, no multi-select.
30
+ */
31
+ export function renderLauncherUI(params: {
32
+ prefill: string;
33
+ }): (
34
+ tui: TUI,
35
+ theme: Theme,
36
+ kb: KeybindingsManager,
37
+ done: (result: SessionLauncherResult | null) => void,
38
+ ) => {
39
+ render: (width: number) => string[];
40
+ invalidate: () => void;
41
+ handleInput: (data: string) => void;
42
+ } {
43
+ return (_tui, theme, _kb, done) => {
44
+ const { prefill } = params;
45
+
46
+ // State
47
+ let optionIndex = 0;
48
+ let cachedLines: string[] | undefined;
49
+
50
+ function refresh() {
51
+ cachedLines = undefined;
52
+ _tui.requestRender();
53
+ }
54
+
55
+ function handleInput(data: string) {
56
+ // Navigation
57
+ if (matchesKey(data, Key.up)) {
58
+ optionIndex = Math.max(0, optionIndex - 1);
59
+ refresh();
60
+ return;
61
+ }
62
+ if (matchesKey(data, Key.down)) {
63
+ optionIndex = Math.min(OPTIONS.length - 1, optionIndex + 1);
64
+ refresh();
65
+ return;
66
+ }
67
+
68
+ // Enter: select
69
+ if (matchesKey(data, Key.enter)) {
70
+ const opt = OPTIONS[optionIndex];
71
+ done({ action: opt.action, prefill });
72
+ return;
73
+ }
74
+
75
+ // Escape: cancel
76
+ if (matchesKey(data, Key.escape)) {
77
+ done(null);
78
+ return;
79
+ }
80
+ }
81
+
82
+ function render(width: number): string[] {
83
+ if (cachedLines) return cachedLines;
84
+
85
+ const lines: string[] = [];
86
+ const innerWidth = Math.max(40, width - 2);
87
+ const border = (s: string) => theme.fg("accent", s);
88
+
89
+ function padVisible(content: string, targetWidth: number): string {
90
+ const vw = visibleWidth(content);
91
+ const pad = Math.max(0, targetWidth - vw);
92
+ return content + " ".repeat(pad);
93
+ }
94
+
95
+ const add = (s: string) =>
96
+ lines.push(
97
+ border("โ”‚") +
98
+ padVisible(truncateToWidth(s, innerWidth), innerWidth) +
99
+ border("โ”‚"),
100
+ );
101
+ const addEmpty = () =>
102
+ lines.push(border("โ”‚") + " ".repeat(innerWidth) + border("โ”‚"));
103
+
104
+ // Top border
105
+ lines.push(border(`โ•ญ${"โ”€".repeat(innerWidth)}โ•ฎ`));
106
+
107
+ // Header: show prefill command (truncated)
108
+ const headerPrefix = " ๐Ÿš€ ";
109
+ const maxPrefillWidth = innerWidth - headerPrefix.length - 1;
110
+ const truncatedPrefill = truncateToWidth(prefill || "(no command)", maxPrefillWidth);
111
+ add(theme.fg("accent", headerPrefix) + theme.fg("text", truncatedPrefill));
112
+ addEmpty();
113
+
114
+ // Options
115
+ for (let i = 0; i < OPTIONS.length; i++) {
116
+ const opt = OPTIONS[i];
117
+ const isSelected = i === optionIndex;
118
+ const prefix = isSelected ? theme.fg("accent", "> ") : " ";
119
+ const label = `${opt.icon} ${opt.label}`;
120
+ const color = isSelected ? "accent" : "text";
121
+ add(prefix + theme.fg(color, label));
122
+ }
123
+
124
+ // Footer hint
125
+ addEmpty();
126
+ add(theme.fg("dim", " โ†‘โ†“ navigate โ€ข Enter select โ€ข Esc cancel"));
127
+
128
+ // Bottom border
129
+ lines.push(border(`โ•ฐ${"โ”€".repeat(innerWidth)}โ•ฏ`));
130
+
131
+ cachedLines = lines;
132
+ return lines;
133
+ }
134
+
135
+ return {
136
+ render,
137
+ invalidate: () => {
138
+ cachedLines = undefined;
139
+ },
140
+ handleInput,
141
+ };
142
+ };
143
+ }
@@ -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 handoff. Returns `new_session` response kind with optional `prefill`. Shows a launcher overlay offering **Compact & run** (compact first, then queue/submit the prefill) or **Run directly** (queue/submit immediately). The current LLM follow-up is aborted after a successful queue or editor fallback. |
60
60
 
61
61
  ## Examples
62
62
 
@@ -149,4 +149,16 @@ ask_user({
149
149
  - "Looks good" returns immediately with selection
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
- - "Start fresh" starts a new session with the prefill message
152
+ - "Start fresh" opens the launcher; **Compact & run** or **Run directly** queues the prefill message automatically
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 | Starts `ctx.compact()` without waiting in the tool spinner, then queues/submits the prefill as a follow-up message after compaction or a short fallback timer |
161
+ | โ–ถ Run directly | Queues/submits the prefill immediately as a follow-up message, without compaction |
162
+ | โœ• Cancel | Cancels the session launch; no prefill is queued |
163
+
164
+ The prefill can be a slash command (for example `/unipi:work specs:...`) or any non-empty message. If automatic delivery fails, ask_user places the prefill in the editor and warns the user to press Enter. This two-step flow lets the user manage context window usage before starting a new task while avoiding unnecessary LLM follow-up in the old session.