@selesai/code 0.5.12 → 0.5.14
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 +186 -0
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +1 -1
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/extensions/caveman/caveman-instructions.cjs +6 -34
- package/dist/extensions/caveman/test/helpers.test.js +7 -13
- package/dist/extensions/package.json +4 -3
- package/dist/extensions/pi-powerline-footer/README.md +1 -1
- package/dist/extensions/pi-powerline-footer/guide.ts +227 -0
- package/dist/extensions/pi-powerline-footer/index.ts +88 -55
- package/dist/extensions/pi-powerline-footer/tests/editor-responsiveness.test.ts +0 -78
- package/dist/extensions/pi-powerline-footer/tests/guide.test.ts +102 -0
- package/dist/extensions/pi-powerline-footer/welcome.ts +65 -30
- package/dist/extensions/ponytail/index.js +1 -1
- package/dist/extensions/ponytail/ponytail-instructions.cjs +15 -1
- package/dist/extensions/ponytail/test/extension.test.js +20 -4
- package/dist/extensions/ponytail/test/helpers.test.js +32 -0
- package/dist/extensions/workflow/adapter.ts +0 -19
- package/dist/skills/caveman/SKILL.md +1 -0
- package/dist/skills/handoff/SKILL.md +1 -0
- package/dist/skills/handoff-text/SKILL.md +1 -0
- package/dist/skills/implanger/SKILL.md +1 -0
- package/dist/skills/improve-codebase/SKILL.md +1 -0
- package/dist/skills/planger/SKILL.md +1 -0
- package/dist/skills/ponytail/SKILL.md +1 -0
- package/dist/skills/ponytail-audit/SKILL.md +1 -0
- package/dist/skills/ponytail-debt/SKILL.md +1 -0
- package/dist/skills/ponytail-gain/SKILL.md +1 -0
- package/dist/skills/ponytail-help/SKILL.md +1 -0
- package/dist/skills/ponytail-review/SKILL.md +1 -0
- package/dist/skills/workflow-creation/SKILL.md +1 -0
- package/package.json +23 -2
- package/dist/agents/architect.md +0 -217
- package/dist/agents/builder.md +0 -120
- package/dist/agents/commentator.md +0 -130
- package/dist/agents/explorer.md +0 -51
- package/dist/agents/recapper.md +0 -26
- package/dist/extensions/gitignore-guard.ts +0 -132
- package/dist/extensions/hooks/claude-codex-hooks.json +0 -44
- package/dist/extensions/hooks/copilot-hooks.json +0 -21
- package/dist/extensions/hooks/ponytail-activate.js +0 -91
- package/dist/extensions/hooks/ponytail-config.js +0 -122
- package/dist/extensions/hooks/ponytail-instructions.js +0 -94
- package/dist/extensions/hooks/ponytail-mode-tracker.js +0 -55
- package/dist/extensions/hooks/ponytail-runtime.js +0 -68
- package/dist/extensions/hooks/ponytail-statusline.ps1 +0 -21
- package/dist/extensions/hooks/ponytail-statusline.sh +0 -12
- package/dist/extensions/hooks/ponytail-subagent.js +0 -22
- package/dist/extensions/pi-extension/index.js +0 -189
- package/dist/extensions/pi-extension/package.json +0 -8
- package/dist/extensions/pi-extension/test/extension.test.js +0 -167
- package/dist/extensions/pi-extension/test/helpers.test.js +0 -92
- package/dist/extensions/pi-powerline-footer/welcome-dismiss.ts +0 -34
- package/dist/extensions/pi-subagents/agents/context-builder.md +0 -46
- package/dist/extensions/pi-subagents/agents/delegate.md +0 -12
- package/dist/extensions/pi-subagents/agents/oracle.md +0 -73
- package/dist/extensions/pi-subagents/agents/planner.md +0 -55
- package/dist/extensions/pi-subagents/agents/reviewer.md +0 -79
- package/dist/extensions/pi-subagents/agents/scout.md +0 -50
- package/dist/extensions/pi-subagents/agents/worker.md +0 -55
- package/dist/extensions/pi-subagents/src/runs/shared/spawn-timing.ts +0 -46
- package/dist/extensions/pi-subagents/test/unit/spawn-timing.test.ts +0 -45
- package/dist/extensions/pi-subagents/tsconfig.runner.json +0 -17
- package/dist/extensions/ponytail-pi-extension/index.js +0 -189
- package/dist/extensions/ponytail-pi-extension/package.json +0 -8
- package/dist/extensions/ponytail-pi-extension/test/extension.test.js +0 -167
- package/dist/extensions/ponytail-pi-extension/test/helpers.test.js +0 -92
- package/dist/extensions/prototype.ts +0 -713
- package/dist/extensions/question.ts +0 -350
- package/dist/extensions/quick.ts +0 -630
- package/dist/extensions/tps-tracker.ts +0 -280
- package/dist/extensions/workflow/task-validators.ts +0 -69
- package/dist/skills/selesai-default/SKILL.md +0 -16
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
CONFIG_DIR_NAME,
|
|
4
4
|
getAgentDir,
|
|
5
5
|
getSettingsPath,
|
|
6
|
+
VERSION,
|
|
6
7
|
type ExtensionAPI,
|
|
7
8
|
type ReadonlyFooterDataProvider,
|
|
8
9
|
type Theme,
|
|
@@ -33,7 +34,7 @@ import { renderSegment } from "./segments.ts";
|
|
|
33
34
|
import { getGitStatus, invalidateGitStatus, invalidateGitBranch } from "./git-status.ts";
|
|
34
35
|
import { ansi, getFgAnsiCode } from "./colors.ts";
|
|
35
36
|
import { WelcomeComponent, WelcomeHeader, discoverLoadedCounts, getRecentSessions } from "./welcome.ts";
|
|
36
|
-
import {
|
|
37
|
+
import { getGuidePreferences, getNewGuideFeatures, markGuideVersionSeen, resolveGuideDisplayMode, saveGuidePreferences, type GuideFeature } from "./guide.ts";
|
|
37
38
|
import { createRenderScheduler } from "./render-scheduler.ts";
|
|
38
39
|
import { readCoreContextUsage } from "./context-usage.ts";
|
|
39
40
|
import { renderFixedEditorCluster } from "./fixed-editor/cluster.ts";
|
|
@@ -940,6 +941,7 @@ export default function powerlineFooter(pi: ExtensionAPI) {
|
|
|
940
941
|
let fixedWidgetContainerBelow: any = null;
|
|
941
942
|
let stashShortcutInputUnsubscribe: (() => void) | null = null;
|
|
942
943
|
let dismissWelcomeOverlay: (() => void) | null = null;
|
|
944
|
+
let guideOverlayBlocking = false;
|
|
943
945
|
let welcomeHeaderActive = false;
|
|
944
946
|
let welcomeOverlayShouldDismiss = false;
|
|
945
947
|
let lastUserPrompt = "";
|
|
@@ -963,11 +965,6 @@ export default function powerlineFooter(pi: ExtensionAPI) {
|
|
|
963
965
|
|
|
964
966
|
const getShellPath = () => process.env.SHELL || "/bin/sh";
|
|
965
967
|
const getShellCwd = () => shellSession?.state.cwd ?? currentCtx?.cwd ?? process.cwd();
|
|
966
|
-
const welcomeDismissScheduler = createWelcomeDismissScheduler({
|
|
967
|
-
dismiss: (ctx: unknown) => dismissWelcome(ctx),
|
|
968
|
-
getGeneration: () => sessionGeneration,
|
|
969
|
-
isEnabled: () => enabled,
|
|
970
|
-
});
|
|
971
968
|
|
|
972
969
|
const statusRenderScheduler = createRenderScheduler(() => {
|
|
973
970
|
const msSinceInput = Date.now() - lastEditorInputAt;
|
|
@@ -1187,6 +1184,13 @@ export default function powerlineFooter(pi: ExtensionAPI) {
|
|
|
1187
1184
|
stashedEditorText = null;
|
|
1188
1185
|
|
|
1189
1186
|
const settings = readSettings(ctx.cwd);
|
|
1187
|
+
const guidePreferences = getGuidePreferences(settings);
|
|
1188
|
+
const guideDisplayMode = resolveGuideDisplayMode({
|
|
1189
|
+
reason: event.reason,
|
|
1190
|
+
hasUI: ctx.hasUI,
|
|
1191
|
+
guideMode: guidePreferences.mode,
|
|
1192
|
+
});
|
|
1193
|
+
const newGuideFeatures = getNewGuideFeatures(guidePreferences.lastSeenVersion, VERSION);
|
|
1190
1194
|
bashModeSettings = parseBashModeSettings(settings);
|
|
1191
1195
|
resolvedShortcuts = resolveShortcutConfig(settings);
|
|
1192
1196
|
showLastPrompt = settings.showLastPrompt !== false;
|
|
@@ -1210,12 +1214,10 @@ export default function powerlineFooter(pi: ExtensionAPI) {
|
|
|
1210
1214
|
|
|
1211
1215
|
if (enabled && ctx.hasUI) {
|
|
1212
1216
|
setupCustomEditor(ctx);
|
|
1213
|
-
if (
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
setupWelcomeOverlay(ctx);
|
|
1218
|
-
}
|
|
1217
|
+
if (guideDisplayMode === "full") {
|
|
1218
|
+
setupWelcomeOverlay(ctx, newGuideFeatures);
|
|
1219
|
+
} else if (guideDisplayMode === "compact") {
|
|
1220
|
+
setupWelcomeHeader(ctx, newGuideFeatures);
|
|
1219
1221
|
} else {
|
|
1220
1222
|
dismissWelcome(ctx);
|
|
1221
1223
|
}
|
|
@@ -1228,9 +1230,9 @@ export default function powerlineFooter(pi: ExtensionAPI) {
|
|
|
1228
1230
|
completedAsyncSubagentUsage.clear();
|
|
1229
1231
|
dismissWelcomeOverlay?.();
|
|
1230
1232
|
dismissWelcomeOverlay = null;
|
|
1233
|
+
guideOverlayBlocking = false;
|
|
1231
1234
|
welcomeHeaderActive = false;
|
|
1232
1235
|
welcomeOverlayShouldDismiss = false;
|
|
1233
|
-
welcomeDismissScheduler.cancel();
|
|
1234
1236
|
statusRenderScheduler.cancel();
|
|
1235
1237
|
restoreFooterStatusRepaintHook?.();
|
|
1236
1238
|
restoreFooterStatusRepaintHook = null;
|
|
@@ -1333,13 +1335,12 @@ export default function powerlineFooter(pi: ExtensionAPI) {
|
|
|
1333
1335
|
}
|
|
1334
1336
|
});
|
|
1335
1337
|
|
|
1336
|
-
// Track streaming state (footer only shows status during streaming)
|
|
1337
|
-
//
|
|
1338
|
+
// Track streaming state (footer only shows status during streaming).
|
|
1339
|
+
// A visible guide remains blocking until its own key handler dismisses it.
|
|
1338
1340
|
pi.on("agent_start", async (_event, ctx) => {
|
|
1339
1341
|
isStreaming = true;
|
|
1340
1342
|
liveAssistantUsage = null;
|
|
1341
1343
|
onVibeAgentStart();
|
|
1342
|
-
dismissWelcome(ctx);
|
|
1343
1344
|
currentCtx = ctx;
|
|
1344
1345
|
});
|
|
1345
1346
|
|
|
@@ -1372,9 +1373,8 @@ export default function powerlineFooter(pi: ExtensionAPI) {
|
|
|
1372
1373
|
requestImmediateStatusRender({ deferDuringTyping: false });
|
|
1373
1374
|
});
|
|
1374
1375
|
|
|
1375
|
-
//
|
|
1376
|
+
// Refresh vibe if rate limit allows. The guide is dismissed only by its input handler.
|
|
1376
1377
|
pi.on("tool_call", async (event, ctx) => {
|
|
1377
|
-
dismissWelcome(ctx);
|
|
1378
1378
|
if (ctx.hasUI) {
|
|
1379
1379
|
// Extract recent agent context from session for richer vibe generation
|
|
1380
1380
|
const agentContext = getRecentAgentContext(ctx);
|
|
@@ -1409,7 +1409,7 @@ export default function powerlineFooter(pi: ExtensionAPI) {
|
|
|
1409
1409
|
}
|
|
1410
1410
|
|
|
1411
1411
|
function dismissWelcome(ctx: any) {
|
|
1412
|
-
|
|
1412
|
+
if (guideOverlayBlocking) return;
|
|
1413
1413
|
|
|
1414
1414
|
if (dismissWelcomeOverlay) {
|
|
1415
1415
|
dismissWelcomeOverlay();
|
|
@@ -1424,9 +1424,20 @@ export default function powerlineFooter(pi: ExtensionAPI) {
|
|
|
1424
1424
|
}
|
|
1425
1425
|
}
|
|
1426
1426
|
|
|
1427
|
-
function
|
|
1428
|
-
|
|
1429
|
-
|
|
1427
|
+
function closeWelcome(ctx: any): void {
|
|
1428
|
+
dismissWelcomeOverlay?.();
|
|
1429
|
+
dismissWelcomeOverlay = null;
|
|
1430
|
+
guideOverlayBlocking = false;
|
|
1431
|
+
if (welcomeHeaderActive) {
|
|
1432
|
+
welcomeHeaderActive = false;
|
|
1433
|
+
ctx.ui.setHeader(undefined);
|
|
1434
|
+
}
|
|
1435
|
+
welcomeOverlayShouldDismiss = false;
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
function currentGuideFeatures(ctx: any): GuideFeature[] {
|
|
1439
|
+
const preferences = getGuidePreferences(readSettings(ctx.cwd));
|
|
1440
|
+
return getNewGuideFeatures(preferences.lastSeenVersion, VERSION);
|
|
1430
1441
|
}
|
|
1431
1442
|
|
|
1432
1443
|
function addStashHistoryEntry(text: string): void {
|
|
@@ -1696,6 +1707,50 @@ export default function powerlineFooter(pi: ExtensionAPI) {
|
|
|
1696
1707
|
requestStatusRender();
|
|
1697
1708
|
});
|
|
1698
1709
|
|
|
1710
|
+
pi.registerCommand("guide", {
|
|
1711
|
+
description: "Open the recurring Selesai feature guide",
|
|
1712
|
+
handler: async (args, ctx) => {
|
|
1713
|
+
if (!ctx.hasUI) return;
|
|
1714
|
+
|
|
1715
|
+
const mode = args?.trim().toLowerCase() ?? "";
|
|
1716
|
+
if (mode !== "" && !["full", "compact", "off", "reset"].includes(mode)) {
|
|
1717
|
+
ctx.ui.notify("Usage: /guide [full|compact|off|reset]", "warning");
|
|
1718
|
+
return;
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
if (!enabled) {
|
|
1722
|
+
ctx.ui.notify("Powerline is disabled; use /powerline first", "info");
|
|
1723
|
+
return;
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
if (mode === "off") {
|
|
1727
|
+
const saved = saveGuidePreferences({ mode: "off" }, getSettingsPath());
|
|
1728
|
+
closeWelcome(ctx);
|
|
1729
|
+
ctx.ui.notify(saved ? "Guide hidden for future sessions" : "Guide hidden for this session; settings.json was not updated", saved ? "info" : "warning");
|
|
1730
|
+
return;
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
if (mode === "compact") {
|
|
1734
|
+
const saved = saveGuidePreferences({ mode: "compact" }, getSettingsPath());
|
|
1735
|
+
closeWelcome(ctx);
|
|
1736
|
+
setupWelcomeHeader(ctx, currentGuideFeatures(ctx));
|
|
1737
|
+
if (!saved) ctx.ui.notify("Compact guide enabled for this session; settings.json was not updated", "warning");
|
|
1738
|
+
return;
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
if (mode === "full" || mode === "reset") {
|
|
1742
|
+
const saved = saveGuidePreferences({
|
|
1743
|
+
mode: "full",
|
|
1744
|
+
...(mode === "reset" ? { lastSeenVersion: null } : {}),
|
|
1745
|
+
}, getSettingsPath());
|
|
1746
|
+
if (!saved) ctx.ui.notify("Guide opened, but settings.json was not updated", "warning");
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
closeWelcome(ctx);
|
|
1750
|
+
setupWelcomeOverlay(ctx, currentGuideFeatures(ctx));
|
|
1751
|
+
},
|
|
1752
|
+
});
|
|
1753
|
+
|
|
1699
1754
|
// Command to toggle/configure
|
|
1700
1755
|
pi.registerCommand("powerline", {
|
|
1701
1756
|
description: "Configure powerline status (toggle, preset)",
|
|
@@ -1718,7 +1773,6 @@ export default function powerlineFooter(pi: ExtensionAPI) {
|
|
|
1718
1773
|
dismissWelcomeOverlay = null;
|
|
1719
1774
|
welcomeHeaderActive = false;
|
|
1720
1775
|
welcomeOverlayShouldDismiss = false;
|
|
1721
|
-
welcomeDismissScheduler.cancel();
|
|
1722
1776
|
getPromptHistoryState().savedPromptHistory = [];
|
|
1723
1777
|
stashedEditorText = null;
|
|
1724
1778
|
ctx.ui.setStatus("stash", undefined);
|
|
@@ -2520,7 +2574,6 @@ export default function powerlineFooter(pi: ExtensionAPI) {
|
|
|
2520
2574
|
}
|
|
2521
2575
|
if (isStashShortcutInput(data)) {
|
|
2522
2576
|
stashOrRestoreEditorText(ctx);
|
|
2523
|
-
scheduleDismissWelcome(ctx);
|
|
2524
2577
|
tuiRef?.requestRender();
|
|
2525
2578
|
return { consume: true };
|
|
2526
2579
|
}
|
|
@@ -2531,7 +2584,6 @@ export default function powerlineFooter(pi: ExtensionAPI) {
|
|
|
2531
2584
|
}
|
|
2532
2585
|
|
|
2533
2586
|
runPowerlineShortcut(ctx, powerlineShortcutAction);
|
|
2534
|
-
scheduleDismissWelcome(ctx);
|
|
2535
2587
|
tuiRef?.requestRender();
|
|
2536
2588
|
return { consume: true };
|
|
2537
2589
|
})
|
|
@@ -2634,14 +2686,12 @@ export default function powerlineFooter(pi: ExtensionAPI) {
|
|
|
2634
2686
|
|
|
2635
2687
|
if (isStashShortcutInput(data)) {
|
|
2636
2688
|
stashOrRestoreEditorText(ctx);
|
|
2637
|
-
scheduleDismissWelcome(ctx);
|
|
2638
2689
|
return;
|
|
2639
2690
|
}
|
|
2640
2691
|
|
|
2641
2692
|
const powerlineShortcutAction = getPowerlineShortcutAction(data);
|
|
2642
2693
|
if (powerlineShortcutAction) {
|
|
2643
2694
|
runPowerlineShortcut(ctx, powerlineShortcutAction);
|
|
2644
|
-
scheduleDismissWelcome(ctx);
|
|
2645
2695
|
return;
|
|
2646
2696
|
}
|
|
2647
2697
|
|
|
@@ -2658,7 +2708,6 @@ export default function powerlineFooter(pi: ExtensionAPI) {
|
|
|
2658
2708
|
|
|
2659
2709
|
attachAutocompleteProvider();
|
|
2660
2710
|
const followUpText = keybindings.matches(data, "app.message.followUp") ? getCurrentEditorText(ctx, editor) : "";
|
|
2661
|
-
scheduleDismissWelcome(ctx);
|
|
2662
2711
|
originalHandleInput(data);
|
|
2663
2712
|
if (hasNonWhitespaceText(followUpText) && !hasNonWhitespaceText(getCurrentEditorText(ctx, editor))) {
|
|
2664
2713
|
followSubmittedEditorToBottom();
|
|
@@ -2746,14 +2795,15 @@ export default function powerlineFooter(pi: ExtensionAPI) {
|
|
|
2746
2795
|
}
|
|
2747
2796
|
}
|
|
2748
2797
|
|
|
2749
|
-
function setupWelcomeHeader(ctx: any) {
|
|
2798
|
+
function setupWelcomeHeader(ctx: any, newFeatures: GuideFeature[] = []) {
|
|
2750
2799
|
const modelName = ctx.model?.name || ctx.model?.id || "No model";
|
|
2751
2800
|
const providerName = ctx.model?.provider || "Unknown";
|
|
2752
2801
|
const loadedCounts = discoverLoadedCounts();
|
|
2753
2802
|
const recentSessions = getRecentSessions(3);
|
|
2754
2803
|
|
|
2755
|
-
const header = new WelcomeHeader(modelName, providerName, recentSessions, loadedCounts);
|
|
2804
|
+
const header = new WelcomeHeader(modelName, providerName, recentSessions, loadedCounts, "compact", newFeatures);
|
|
2756
2805
|
welcomeHeaderActive = true;
|
|
2806
|
+
markGuideVersionSeen(VERSION, getSettingsPath());
|
|
2757
2807
|
|
|
2758
2808
|
ctx.ui.setHeader(() => {
|
|
2759
2809
|
return {
|
|
@@ -2767,7 +2817,7 @@ export default function powerlineFooter(pi: ExtensionAPI) {
|
|
|
2767
2817
|
});
|
|
2768
2818
|
}
|
|
2769
2819
|
|
|
2770
|
-
function setupWelcomeOverlay(ctx: any) {
|
|
2820
|
+
function setupWelcomeOverlay(ctx: any, newFeatures: GuideFeature[] = []) {
|
|
2771
2821
|
const modelName = ctx.model?.name || ctx.model?.id || "No model";
|
|
2772
2822
|
const providerName = ctx.model?.provider || "Unknown";
|
|
2773
2823
|
const loadedCounts = discoverLoadedCounts();
|
|
@@ -2782,45 +2832,29 @@ export default function powerlineFooter(pi: ExtensionAPI) {
|
|
|
2782
2832
|
return;
|
|
2783
2833
|
}
|
|
2784
2834
|
|
|
2785
|
-
const sessionEvents = ctx.sessionManager?.getBranch?.() ?? [];
|
|
2786
|
-
const hasActivity = sessionEvents.some((entry: unknown) => {
|
|
2787
|
-
if (!isRecord(entry)) return false;
|
|
2788
|
-
if (entry.type === "tool_call" || entry.type === "tool_result") return true;
|
|
2789
|
-
return entry.type === "message" && isRecord(entry.message) && entry.message.role === "assistant";
|
|
2790
|
-
});
|
|
2791
|
-
if (hasActivity) {
|
|
2792
|
-
return;
|
|
2793
|
-
}
|
|
2794
|
-
|
|
2795
2835
|
ctx.ui.custom(
|
|
2796
|
-
(
|
|
2836
|
+
(_tui: any, _theme: any, _keybindings: any, done: (result: void) => void) => {
|
|
2797
2837
|
const welcome = new WelcomeComponent(
|
|
2798
2838
|
modelName,
|
|
2799
2839
|
providerName,
|
|
2800
2840
|
recentSessions,
|
|
2801
2841
|
loadedCounts,
|
|
2842
|
+
"full",
|
|
2843
|
+
newFeatures,
|
|
2802
2844
|
);
|
|
2845
|
+
markGuideVersionSeen(VERSION, getSettingsPath());
|
|
2846
|
+
guideOverlayBlocking = true;
|
|
2803
2847
|
|
|
2804
|
-
let countdown = 30;
|
|
2805
2848
|
let dismissed = false;
|
|
2806
|
-
let interval: ReturnType<typeof setInterval> | null = null;
|
|
2807
2849
|
|
|
2808
2850
|
const dismiss = () => {
|
|
2809
2851
|
if (dismissed) return;
|
|
2810
2852
|
dismissed = true;
|
|
2811
|
-
if (interval) clearInterval(interval);
|
|
2812
2853
|
dismissWelcomeOverlay = null;
|
|
2854
|
+
guideOverlayBlocking = false;
|
|
2813
2855
|
done();
|
|
2814
2856
|
};
|
|
2815
2857
|
|
|
2816
|
-
interval = setInterval(() => {
|
|
2817
|
-
if (dismissed) return;
|
|
2818
|
-
countdown--;
|
|
2819
|
-
welcome.setCountdown(countdown);
|
|
2820
|
-
tui.requestRender();
|
|
2821
|
-
if (countdown <= 0) dismiss();
|
|
2822
|
-
}, 1000);
|
|
2823
|
-
|
|
2824
2858
|
dismissWelcomeOverlay = dismiss;
|
|
2825
2859
|
|
|
2826
2860
|
if (welcomeOverlayShouldDismiss) {
|
|
@@ -2829,13 +2863,12 @@ export default function powerlineFooter(pi: ExtensionAPI) {
|
|
|
2829
2863
|
}
|
|
2830
2864
|
|
|
2831
2865
|
return {
|
|
2832
|
-
focused:
|
|
2866
|
+
focused: true,
|
|
2833
2867
|
invalidate: () => welcome.invalidate(),
|
|
2834
2868
|
render: (width: number) => welcome.render(width),
|
|
2835
2869
|
handleInput: () => dismiss(),
|
|
2836
2870
|
dispose: () => {
|
|
2837
2871
|
dismissed = true;
|
|
2838
|
-
if (interval) clearInterval(interval);
|
|
2839
2872
|
},
|
|
2840
2873
|
};
|
|
2841
2874
|
},
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
-
import { createWelcomeDismissScheduler } from "../welcome-dismiss.ts";
|
|
4
3
|
import { createRenderScheduler } from "../render-scheduler.ts";
|
|
5
4
|
|
|
6
5
|
test("render scheduler coalesces pending status renders", () => {
|
|
@@ -101,80 +100,3 @@ test("render scheduler cancels pending status renders", () => {
|
|
|
101
100
|
globalThis.clearTimeout = originalClearTimeout;
|
|
102
101
|
}
|
|
103
102
|
});
|
|
104
|
-
|
|
105
|
-
test("welcome dismissal scheduler coalesces pending work", () => {
|
|
106
|
-
const originalSetTimeout = globalThis.setTimeout;
|
|
107
|
-
const callbacks: Array<() => void> = [];
|
|
108
|
-
let dismissCount = 0;
|
|
109
|
-
|
|
110
|
-
globalThis.setTimeout = ((callback: () => void) => {
|
|
111
|
-
callbacks.push(callback);
|
|
112
|
-
return { id: callbacks.length } as ReturnType<typeof setTimeout>;
|
|
113
|
-
}) as typeof setTimeout;
|
|
114
|
-
|
|
115
|
-
try {
|
|
116
|
-
const scheduler = createWelcomeDismissScheduler({
|
|
117
|
-
dismiss: () => { dismissCount += 1; },
|
|
118
|
-
getGeneration: () => 1,
|
|
119
|
-
isEnabled: () => true,
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
scheduler.schedule(undefined);
|
|
123
|
-
scheduler.schedule(undefined);
|
|
124
|
-
scheduler.schedule(undefined);
|
|
125
|
-
|
|
126
|
-
assert.equal(callbacks.length, 1);
|
|
127
|
-
callbacks[0]?.();
|
|
128
|
-
assert.equal(dismissCount, 1);
|
|
129
|
-
} finally {
|
|
130
|
-
globalThis.setTimeout = originalSetTimeout;
|
|
131
|
-
}
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
test("welcome dismissal scheduler cancels stale pending work", () => {
|
|
135
|
-
const originalSetTimeout = globalThis.setTimeout;
|
|
136
|
-
const originalClearTimeout = globalThis.clearTimeout;
|
|
137
|
-
const callbacks: Array<() => void> = [];
|
|
138
|
-
const cleared = new Set<object>();
|
|
139
|
-
let dismissCount = 0;
|
|
140
|
-
let generation = 1;
|
|
141
|
-
let enabled = true;
|
|
142
|
-
|
|
143
|
-
globalThis.setTimeout = ((callback: () => void) => {
|
|
144
|
-
const handle = { id: callbacks.length + 1 };
|
|
145
|
-
callbacks.push(callback);
|
|
146
|
-
return handle as ReturnType<typeof setTimeout>;
|
|
147
|
-
}) as typeof setTimeout;
|
|
148
|
-
globalThis.clearTimeout = ((handle?: ReturnType<typeof setTimeout>) => {
|
|
149
|
-
if (handle && typeof handle === "object") {
|
|
150
|
-
cleared.add(handle);
|
|
151
|
-
}
|
|
152
|
-
}) as typeof clearTimeout;
|
|
153
|
-
|
|
154
|
-
try {
|
|
155
|
-
const scheduler = createWelcomeDismissScheduler({
|
|
156
|
-
dismiss: () => { dismissCount += 1; },
|
|
157
|
-
getGeneration: () => generation,
|
|
158
|
-
isEnabled: () => enabled,
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
scheduler.schedule(undefined);
|
|
162
|
-
generation += 1;
|
|
163
|
-
callbacks[0]?.();
|
|
164
|
-
assert.equal(dismissCount, 0);
|
|
165
|
-
|
|
166
|
-
scheduler.schedule(undefined);
|
|
167
|
-
enabled = false;
|
|
168
|
-
callbacks[1]?.();
|
|
169
|
-
assert.equal(dismissCount, 0);
|
|
170
|
-
|
|
171
|
-
enabled = true;
|
|
172
|
-
scheduler.schedule(undefined);
|
|
173
|
-
scheduler.cancel();
|
|
174
|
-
assert.equal(cleared.size, 1);
|
|
175
|
-
assert.equal(dismissCount, 0);
|
|
176
|
-
} finally {
|
|
177
|
-
globalThis.setTimeout = originalSetTimeout;
|
|
178
|
-
globalThis.clearTimeout = originalClearTimeout;
|
|
179
|
-
}
|
|
180
|
-
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import {
|
|
7
|
+
GUIDE_COMPACT_LINES,
|
|
8
|
+
GUIDE_DISMISS_HINT,
|
|
9
|
+
GUIDE_FEATURES,
|
|
10
|
+
getGuideFeatureLines,
|
|
11
|
+
getGuidePreferences,
|
|
12
|
+
getNewGuideFeatures,
|
|
13
|
+
resolveGuideDisplayMode,
|
|
14
|
+
saveGuidePreferences,
|
|
15
|
+
type GuideFeature,
|
|
16
|
+
} from "../guide.ts";
|
|
17
|
+
|
|
18
|
+
const features: GuideFeature[] = [
|
|
19
|
+
{ id: "old", title: "Old", example: "/old", introducedIn: "0.5.0" },
|
|
20
|
+
{ id: "new", title: "New", example: "/new", introducedIn: "0.5.3" },
|
|
21
|
+
{ id: "future", title: "Future", example: "/future", introducedIn: "0.6.0" },
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
test("guide opens fully for each real session start", () => {
|
|
26
|
+
for (const reason of ["startup", "new", "resume", "fork"]) {
|
|
27
|
+
assert.equal(resolveGuideDisplayMode({ reason, hasUI: true, guideMode: "full" }), "full");
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("guide dismissal and compact preferences change automatic display", () => {
|
|
32
|
+
assert.equal(resolveGuideDisplayMode({ reason: "startup", hasUI: true, guideMode: "compact" }), "compact");
|
|
33
|
+
assert.equal(resolveGuideDisplayMode({ reason: "startup", hasUI: true, guideMode: "off" }), "none");
|
|
34
|
+
assert.equal(resolveGuideDisplayMode({ reason: "reload", hasUI: true, guideMode: "full" }), "none");
|
|
35
|
+
assert.equal(resolveGuideDisplayMode({ reason: "startup", hasUI: false, guideMode: "full" }), "none");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("quiet startup does not suppress the default blocking guide", () => {
|
|
39
|
+
assert.equal(resolveGuideDisplayMode({ reason: "startup", hasUI: true, guideMode: "full" }), "full");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("full and compact guide content expose README commands and examples", () => {
|
|
43
|
+
const full = getGuideFeatureLines()
|
|
44
|
+
.map((line) => line.kind === "section" ? line.text : `${line.feature.title}: ${line.feature.example}`)
|
|
45
|
+
.join("\n");
|
|
46
|
+
const compact = GUIDE_COMPACT_LINES.join("\n");
|
|
47
|
+
|
|
48
|
+
assert.match(full, /\/settings/);
|
|
49
|
+
assert.match(full, /\/workflow-task/);
|
|
50
|
+
assert.match(full, /\/workflow-prototype/);
|
|
51
|
+
assert.match(full, /Ask the architect to challenge this plan/);
|
|
52
|
+
assert.match(full, /Ask the researcher to research <topic> and cite sources/);
|
|
53
|
+
assert.match(full, /\/skill:\*/);
|
|
54
|
+
assert.match(full, /\/handoff-new Next: fix <problem>; start with a coding plan/);
|
|
55
|
+
assert.doesNotMatch(full, /question\(|\/intercom|powerline/);
|
|
56
|
+
assert.match(compact, /\/settings/);
|
|
57
|
+
assert.match(compact, /\/workflow-task/);
|
|
58
|
+
assert.match(compact, /Ask the researcher to research <topic>/);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("blocking guide dismisses only from its overlay input handler", () => {
|
|
62
|
+
const source = readFileSync(new URL("../index.ts", import.meta.url), "utf8");
|
|
63
|
+
|
|
64
|
+
assert.equal(GUIDE_DISMISS_HINT, "Press any key to continue");
|
|
65
|
+
assert.match(source, /guideOverlayBlocking = true;[\s\S]*handleInput: \(\) => dismiss\(\),/);
|
|
66
|
+
assert.doesNotMatch(source, /scheduleDismissWelcome|welcomeDismissScheduler|welcome-dismiss/);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("README-derived guide entries stay in the feature map", () => {
|
|
70
|
+
assert.deepEqual(
|
|
71
|
+
GUIDE_FEATURES.filter((feature) => feature.id === "settings" || feature.id === "handoff").map((feature) => feature.example),
|
|
72
|
+
["/settings · /guide compact|off|reset · /reload", "/handoff-new Next: fix <problem>; start with a coding plan."],
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("new feature detection is version bounded and empty on first install", () => {
|
|
77
|
+
assert.deepEqual(getNewGuideFeatures(undefined, "0.5.3", features), []);
|
|
78
|
+
assert.deepEqual(getNewGuideFeatures("0.5.0", "0.5.3", features).map((feature) => feature.id), ["new"]);
|
|
79
|
+
assert.deepEqual(getNewGuideFeatures("0.5.3", "0.5.3", features), []);
|
|
80
|
+
assert.deepEqual(getNewGuideFeatures("0.5.0", "0.5.2", features), []);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("guide dismissal persists only the mode and version marker", () => {
|
|
84
|
+
const dir = mkdtempSync(join(tmpdir(), "selesai-guide-"));
|
|
85
|
+
const settingsPath = join(dir, "settings.json");
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
assert.equal(saveGuidePreferences({ mode: "off", lastSeenVersion: "0.5.3" }, settingsPath), true);
|
|
89
|
+
const settings = JSON.parse(readFileSync(settingsPath, "utf8")) as Record<string, unknown>;
|
|
90
|
+
assert.deepEqual(settings.selesaiGuide, { mode: "off", lastSeenVersion: "0.5.3" });
|
|
91
|
+
assert.equal(getGuidePreferences(settings).mode, "off");
|
|
92
|
+
} finally {
|
|
93
|
+
rmSync(dir, { recursive: true, force: true });
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("invalid persisted guide settings fall back to the full guide", () => {
|
|
98
|
+
assert.deepEqual(getGuidePreferences({ selesaiGuide: { mode: "unexpected", lastSeenVersion: 42 } }), {
|
|
99
|
+
mode: "full",
|
|
100
|
+
lastSeenVersion: undefined,
|
|
101
|
+
});
|
|
102
|
+
});
|