@pi-unipi/unipi 2.0.2 → 2.0.4
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 +10 -3
- package/package.json +2 -2
- package/packages/ask-user/README.md +4 -0
- package/packages/ask-user/ask-ui.ts +97 -90
- package/packages/ask-user/tools.ts +12 -10
- package/packages/autocomplete/README.md +36 -0
- package/packages/compactor/README.md +290 -73
- package/packages/compactor/skills/compactor/SKILL.md +2 -3
- package/packages/compactor/skills/compactor-detail/SKILL.md +49 -64
- package/packages/compactor/skills/compactor-doctor/SKILL.md +28 -31
- package/packages/compactor/skills/compactor-stats/SKILL.md +22 -20
- package/packages/compactor/src/commands/index.ts +4 -1
- package/packages/compactor/src/compaction/auto-trigger.ts +306 -0
- package/packages/compactor/src/config/manager.ts +1 -0
- package/packages/compactor/src/config/presets.ts +26 -0
- package/packages/compactor/src/config/schema.ts +7 -0
- package/packages/compactor/src/index.ts +74 -1
- package/packages/compactor/src/tools/context-budget.ts +18 -2
- package/packages/compactor/src/tools/register.ts +19 -11
- package/packages/compactor/src/tui/settings-overlay.ts +142 -3
- package/packages/compactor/src/types.ts +17 -0
- package/packages/core/events.ts +2 -0
- package/packages/notify/README.md +2 -2
- package/packages/notify/commands.ts +9 -4
- package/packages/notify/events.ts +12 -2
- package/packages/notify/platforms/focus-win.ts +123 -0
- package/packages/notify/platforms/focus.ts +33 -0
- package/packages/notify/platforms/native.ts +33 -1
- package/packages/notify/settings.ts +1 -0
- package/packages/notify/tui/settings-overlay.ts +33 -7
- package/packages/notify/types.ts +8 -0
- package/packages/workflow/README.md +2 -0
- package/packages/workflow/commands.ts +28 -11
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/notify — Focus detection abstraction
|
|
3
|
+
*
|
|
4
|
+
* Unified interface for checking whether the terminal window is the
|
|
5
|
+
* foreground (active) window. Platform-specific implementations are
|
|
6
|
+
* dispatched based on process.platform.
|
|
7
|
+
*
|
|
8
|
+
* Currently implemented:
|
|
9
|
+
* - Windows (win32): calls focus-win.ts
|
|
10
|
+
*
|
|
11
|
+
* Unimplemented platforms always return false (no suppression).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { isWindowFocusedOnWindows } from "./focus-win.js";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Check whether the current terminal/console window is the foreground
|
|
18
|
+
* (active) window. Used by sendNativeNotification to optionally
|
|
19
|
+
* suppress notifications when the user is already looking at the screen.
|
|
20
|
+
*
|
|
21
|
+
* @returns true if the terminal is the foreground window, false otherwise.
|
|
22
|
+
* On unimplemented platforms, always returns false.
|
|
23
|
+
*/
|
|
24
|
+
export async function isWindowFocused(): Promise<boolean> {
|
|
25
|
+
switch (process.platform) {
|
|
26
|
+
case "win32":
|
|
27
|
+
return await isWindowFocusedOnWindows();
|
|
28
|
+
// TODO: macOS — use osascript to check frontmost application
|
|
29
|
+
// TODO: Linux — use xdotool (X11) or per-compositor tool (Wayland)
|
|
30
|
+
default:
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -8,22 +8,54 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import notifier from "node-notifier";
|
|
11
|
+
import { isWindowFocused } from "./focus.js";
|
|
11
12
|
|
|
12
13
|
/** Options for native notification */
|
|
13
14
|
export interface NativeNotificationOptions {
|
|
14
15
|
/** Windows appID to show instead of "SnoreToast" */
|
|
15
16
|
windowsAppId?: string;
|
|
17
|
+
/**
|
|
18
|
+
* When true, suppresses the notification if the terminal window is
|
|
19
|
+
* the foreground (active) window. Only effective on platforms where
|
|
20
|
+
* `isWindowFocused` is implemented (currently Windows).
|
|
21
|
+
*/
|
|
22
|
+
suppressWhenFocused?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Thrown by sendNativeNotification when the notification was suppressed
|
|
27
|
+
* because suppressWhenFocused is set and the terminal window is focused.
|
|
28
|
+
*
|
|
29
|
+
* Callers should catch this and treat it as intentional suppression,
|
|
30
|
+
* NOT as a send failure.
|
|
31
|
+
*/
|
|
32
|
+
export class SuppressedError extends Error {
|
|
33
|
+
constructor() {
|
|
34
|
+
super("Notification suppressed: terminal window is focused");
|
|
35
|
+
this.name = "SuppressedError";
|
|
36
|
+
}
|
|
16
37
|
}
|
|
17
38
|
|
|
18
39
|
/**
|
|
19
40
|
* Send a native OS notification.
|
|
20
|
-
*
|
|
41
|
+
*
|
|
42
|
+
* When `suppressWhenFocused` is true and `isWindowFocused()` returns true
|
|
43
|
+
* (i.e. the terminal is the foreground window), the notification is
|
|
44
|
+
* suppressed and the promise rejects with SuppressedError.
|
|
45
|
+
*
|
|
46
|
+
* Resolves when notification is shown, rejects with SuppressedError on
|
|
47
|
+
* suppression or with a standard Error on failure.
|
|
21
48
|
*/
|
|
22
49
|
export async function sendNativeNotification(
|
|
23
50
|
title: string,
|
|
24
51
|
message: string,
|
|
25
52
|
options?: NativeNotificationOptions
|
|
26
53
|
): Promise<void> {
|
|
54
|
+
// Suppress if the terminal window is currently focused
|
|
55
|
+
if (options?.suppressWhenFocused && await isWindowFocused()) {
|
|
56
|
+
throw new SuppressedError();
|
|
57
|
+
}
|
|
58
|
+
|
|
27
59
|
return new Promise((resolve, reject) => {
|
|
28
60
|
notifier.notify(
|
|
29
61
|
{
|
|
@@ -85,7 +85,7 @@ export class NotifySettingsOverlay implements Component {
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
private get maxItems(): number {
|
|
88
|
-
if (this.section === "platforms") return
|
|
88
|
+
if (this.section === "platforms") return 5; // native, gotify, telegram, ntfy + suppress option
|
|
89
89
|
if (this.section === "recap") return 1; // toggle
|
|
90
90
|
return Object.keys(this.config.events).length;
|
|
91
91
|
}
|
|
@@ -98,12 +98,17 @@ export class NotifySettingsOverlay implements Component {
|
|
|
98
98
|
"telegram",
|
|
99
99
|
"ntfy",
|
|
100
100
|
];
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
101
|
+
if (this.selectedIndex < platforms.length) {
|
|
102
|
+
const key = platforms[this.selectedIndex];
|
|
103
|
+
if (key === "ntfy") {
|
|
104
|
+
// ntfy toggle updates the resolved ntfy config
|
|
105
|
+
this.ntfyConfig.enabled = !this.ntfyConfig.enabled;
|
|
106
|
+
} else if (key) {
|
|
107
|
+
this.config[key].enabled = !this.config[key].enabled;
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
// suppressWhenFocused toggle (index 4)
|
|
111
|
+
this.config.native.suppressWhenFocused = !this.config.native.suppressWhenFocused;
|
|
107
112
|
}
|
|
108
113
|
} else if (this.section === "recap") {
|
|
109
114
|
this.config.recap.enabled = !this.config.recap.enabled;
|
|
@@ -273,6 +278,27 @@ export class NotifySettingsOverlay implements Component {
|
|
|
273
278
|
)
|
|
274
279
|
);
|
|
275
280
|
}
|
|
281
|
+
|
|
282
|
+
// suppressWhenFocused toggle (index 4)
|
|
283
|
+
{
|
|
284
|
+
const i = platforms.length;
|
|
285
|
+
const isSelected = i === this.selectedIndex;
|
|
286
|
+
const isEnabled = this.config.native.suppressWhenFocused === true;
|
|
287
|
+
const toggleOn = this.fg("success", "●");
|
|
288
|
+
const toggleOff = this.fg("dim", "○");
|
|
289
|
+
const toggle = isEnabled ? toggleOn : toggleOff;
|
|
290
|
+
const label = isSelected
|
|
291
|
+
? this.bold("Suppress when focused")
|
|
292
|
+
: this.fg("dim", "Suppress when focused");
|
|
293
|
+
const detail = this.fg("dim", isEnabled ? "Windows only — terminal in foreground → skip" : "Windows only");
|
|
294
|
+
|
|
295
|
+
lines.push(
|
|
296
|
+
this.frameLine(
|
|
297
|
+
`${isSelected ? this.fg("accent", "▸") : " "} ${toggle} ${label} ${detail}`,
|
|
298
|
+
innerWidth
|
|
299
|
+
)
|
|
300
|
+
);
|
|
301
|
+
}
|
|
276
302
|
}
|
|
277
303
|
|
|
278
304
|
private renderEvents(lines: string[], innerWidth: number): void {
|
package/packages/notify/types.ts
CHANGED
|
@@ -19,6 +19,12 @@ export interface NativeConfig {
|
|
|
19
19
|
enabled: boolean;
|
|
20
20
|
/** Windows appID to show instead of "SnoreToast" */
|
|
21
21
|
windowsAppId?: string;
|
|
22
|
+
/**
|
|
23
|
+
* When true, suppresses the notification if the terminal window is the
|
|
24
|
+
* foreground (active) window. Only effective on supported platforms
|
|
25
|
+
* (currently Windows). Default: false.
|
|
26
|
+
*/
|
|
27
|
+
suppressWhenFocused?: boolean;
|
|
22
28
|
}
|
|
23
29
|
|
|
24
30
|
/** Gotify notification platform config */
|
|
@@ -101,6 +107,8 @@ export interface NotifyResult {
|
|
|
101
107
|
platform: NotifyPlatform;
|
|
102
108
|
/** Whether the send succeeded */
|
|
103
109
|
success: boolean;
|
|
110
|
+
/** True when the notification was intentionally suppressed (e.g. window focused) */
|
|
111
|
+
suppressed?: boolean;
|
|
104
112
|
/** Error message if failed */
|
|
105
113
|
error?: string;
|
|
106
114
|
}
|
|
@@ -97,6 +97,8 @@ For small tasks that skip the full flow:
|
|
|
97
97
|
/unipi:worktree-merge feat/new-feature
|
|
98
98
|
```
|
|
99
99
|
|
|
100
|
+
Worktree command arguments autocomplete from `.unipi/worktrees`. Suggestions are cached after the first scan in a Pi session so large worktree directories do not slow down repeated `/unipi:worktree-merge` completions.
|
|
101
|
+
|
|
100
102
|
## Special Triggers
|
|
101
103
|
|
|
102
104
|
Workflow skills detect installed packages and enhance their behavior automatically. This is the coexists system — each package adds capabilities without requiring configuration.
|
|
@@ -10,6 +10,8 @@ import { readFileSync, readdirSync, existsSync, statSync } from "fs";
|
|
|
10
10
|
import { join, basename } from "path";
|
|
11
11
|
import { UNIPI_PREFIX, WORKFLOW_COMMANDS, getToolsForCommand, getSandboxLevel, type SandboxLevel } from "@pi-unipi/core";
|
|
12
12
|
|
|
13
|
+
type CompletionItem = { value: string; label: string; description: string };
|
|
14
|
+
|
|
13
15
|
/** Options for command registration */
|
|
14
16
|
export interface WorkflowCommandOptions {
|
|
15
17
|
/** Check if ralph module is detected */
|
|
@@ -36,7 +38,7 @@ interface WorkflowCommand {
|
|
|
36
38
|
/**
|
|
37
39
|
* Suggest spec files from .unipi/docs/specs/ for plan command.
|
|
38
40
|
*/
|
|
39
|
-
function suggestSpecFiles(prefix: string):
|
|
41
|
+
function suggestSpecFiles(prefix: string): CompletionItem[] {
|
|
40
42
|
const specsDir = join(process.cwd(), ".unipi", "docs", "specs");
|
|
41
43
|
if (!existsSync(specsDir)) return [];
|
|
42
44
|
|
|
@@ -61,7 +63,7 @@ function suggestSpecFiles(prefix: string): { value: string; label: string; descr
|
|
|
61
63
|
/**
|
|
62
64
|
* Suggest plan files from .unipi/docs/plans/ for work and review-work commands.
|
|
63
65
|
*/
|
|
64
|
-
function suggestPlanFiles(prefix: string):
|
|
66
|
+
function suggestPlanFiles(prefix: string): CompletionItem[] {
|
|
65
67
|
const plansDir = join(process.cwd(), ".unipi", "docs", "plans");
|
|
66
68
|
if (!existsSync(plansDir)) return [];
|
|
67
69
|
|
|
@@ -86,7 +88,7 @@ function suggestPlanFiles(prefix: string): { value: string; label: string; descr
|
|
|
86
88
|
/**
|
|
87
89
|
* Suggest debug files from .unipi/docs/debug/ for fix command.
|
|
88
90
|
*/
|
|
89
|
-
function suggestDebugFiles(prefix: string):
|
|
91
|
+
function suggestDebugFiles(prefix: string): CompletionItem[] {
|
|
90
92
|
const debugDir = join(process.cwd(), ".unipi", "docs", "debug");
|
|
91
93
|
if (!existsSync(debugDir)) return [];
|
|
92
94
|
|
|
@@ -111,7 +113,7 @@ function suggestDebugFiles(prefix: string): { value: string; label: string; desc
|
|
|
111
113
|
/**
|
|
112
114
|
* Suggest chore files from .unipi/docs/chore/ for chore-execute command.
|
|
113
115
|
*/
|
|
114
|
-
function suggestChoreFiles(prefix: string):
|
|
116
|
+
function suggestChoreFiles(prefix: string): CompletionItem[] {
|
|
115
117
|
const choreDir = join(process.cwd(), ".unipi", "docs", "chore");
|
|
116
118
|
if (!existsSync(choreDir)) return [];
|
|
117
119
|
|
|
@@ -133,16 +135,29 @@ function suggestChoreFiles(prefix: string): { value: string; label: string; desc
|
|
|
133
135
|
}
|
|
134
136
|
}
|
|
135
137
|
|
|
138
|
+
/** Cached per-cwd worktree suggestions. Worktree autocomplete can be invoked on every
|
|
139
|
+
* keystroke, so the recursive scan is intentionally paid only once per session/cwd.
|
|
140
|
+
*/
|
|
141
|
+
let worktreeSuggestionsCache: { cwd: string; items: CompletionItem[] } | null = null;
|
|
142
|
+
|
|
136
143
|
/**
|
|
137
144
|
* Suggest existing worktree names for merge/list commands.
|
|
138
145
|
* Recursively scans for actual git worktrees (directories containing .git files).
|
|
139
146
|
*/
|
|
140
|
-
function suggestWorktrees():
|
|
141
|
-
const
|
|
142
|
-
if (
|
|
147
|
+
function suggestWorktrees(): CompletionItem[] {
|
|
148
|
+
const cwd = process.cwd();
|
|
149
|
+
if (worktreeSuggestionsCache?.cwd === cwd) {
|
|
150
|
+
return worktreeSuggestionsCache.items;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const worktreesDir = join(cwd, ".unipi", "worktrees");
|
|
154
|
+
if (!existsSync(worktreesDir)) {
|
|
155
|
+
worktreeSuggestionsCache = { cwd, items: [] };
|
|
156
|
+
return worktreeSuggestionsCache.items;
|
|
157
|
+
}
|
|
143
158
|
|
|
144
159
|
try {
|
|
145
|
-
const results:
|
|
160
|
+
const results: CompletionItem[] = [];
|
|
146
161
|
|
|
147
162
|
/**
|
|
148
163
|
* Recursively find worktree directories (those containing a .git file).
|
|
@@ -176,9 +191,11 @@ function suggestWorktrees(): { value: string; label: string; description: string
|
|
|
176
191
|
}
|
|
177
192
|
|
|
178
193
|
findWorktrees(worktreesDir, "");
|
|
179
|
-
|
|
194
|
+
worktreeSuggestionsCache = { cwd, items: results };
|
|
195
|
+
return worktreeSuggestionsCache.items;
|
|
180
196
|
} catch {
|
|
181
|
-
|
|
197
|
+
worktreeSuggestionsCache = { cwd, items: [] };
|
|
198
|
+
return worktreeSuggestionsCache.items;
|
|
182
199
|
}
|
|
183
200
|
}
|
|
184
201
|
|
|
@@ -334,7 +351,7 @@ export function registerWorkflowCommands(
|
|
|
334
351
|
pi.registerCommand(fullCommand, {
|
|
335
352
|
description: cmd.description,
|
|
336
353
|
getArgumentCompletions: (prefix: string) => {
|
|
337
|
-
let items:
|
|
354
|
+
let items: CompletionItem[] | null = null;
|
|
338
355
|
|
|
339
356
|
// Plan command: suggest spec files
|
|
340
357
|
if (cmd.name === WORKFLOW_COMMANDS.PLAN) {
|