@selesai/code 0.1.19 → 0.2.1
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/dist/agents/architect.md +4 -3
- package/dist/agents/builder.md +5 -4
- package/dist/agents/commentator.md +3 -2
- package/dist/agents/explorer.md +4 -4
- package/dist/agents/recapper.md +4 -3
- package/dist/config.d.ts +6 -7
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +7 -9
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +3 -1
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +18 -24
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +2 -0
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/types.d.ts +8 -1
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/skills.d.ts.map +1 -1
- package/dist/core/skills.js +25 -11
- package/dist/core/skills.js.map +1 -1
- package/dist/defaults/models.json +143 -39
- package/dist/defaults/settings.json +2 -29
- package/dist/extensions/package.json +5 -4
- package/dist/extensions/pi-intercom/index.ts +12 -5
- package/dist/extensions/pi-powerline-footer/index.ts +4 -1
- package/dist/extensions/pi-powerline-footer/tps.ts +286 -0
- package/dist/extensions/pi-powerline-footer/welcome.ts +47 -46
- package/dist/extensions/pi-subagents/README.md +12 -5
- package/dist/extensions/pi-subagents/agents/context-builder.md +2 -2
- package/dist/extensions/pi-subagents/agents/researcher.md +4 -5
- package/dist/extensions/pi-subagents/package.json +8 -5
- package/dist/extensions/pi-subagents/prompts/parallel-context-build.md +1 -1
- package/dist/extensions/pi-subagents/prompts/parallel-handoff-plan.md +1 -1
- package/dist/extensions/pi-subagents/src/agents/agent-management.ts +1 -1
- package/dist/extensions/pi-subagents/src/agents/agents.ts +3 -3
- package/dist/extensions/pi-subagents/src/extension/control-notices.ts +1 -1
- package/dist/extensions/pi-subagents/src/extension/doctor.ts +3 -3
- package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +1 -1
- package/dist/extensions/pi-subagents/src/extension/index.ts +29 -2
- package/dist/extensions/pi-subagents/src/profiles/profiles.ts +2 -2
- package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +72 -13
- package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +1 -1
- package/dist/extensions/pi-subagents/src/runs/background/notify.ts +1 -1
- package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +85 -41
- package/dist/extensions/pi-subagents/src/runs/foreground/chain-clarify.ts +1 -1
- package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +1 -1
- package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +46 -3
- package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +2 -2
- package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +1 -3
- package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +2 -2
- package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +18 -18
- package/dist/extensions/pi-subagents/src/runs/shared/pi-spawn.ts +24 -24
- package/dist/extensions/pi-subagents/src/runs/shared/spawn-timing.ts +46 -0
- package/dist/extensions/pi-subagents/src/runs/shared/structured-output.ts +2 -2
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +4 -4
- package/dist/extensions/pi-subagents/src/shared/fork-context.ts +1 -1
- package/dist/extensions/pi-subagents/src/shared/types.ts +6 -6
- package/dist/extensions/pi-subagents/src/shared/utils.ts +3 -3
- package/dist/extensions/pi-subagents/src/slash/slash-bridge.ts +1 -1
- package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +1 -1
- package/dist/extensions/pi-subagents/src/tui/render-helpers.ts +1 -1
- package/dist/extensions/pi-subagents/src/tui/render.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/spawn-timing.test.ts +45 -0
- package/dist/extensions/pi-subagents/tsconfig.runner.json +17 -0
- package/dist/extensions/pi-web-agent/package.json +31 -0
- package/dist/extensions/pi-web-agent/src/backends/config.ts +205 -0
- package/dist/extensions/pi-web-agent/src/backends/doctor.ts +136 -0
- package/dist/extensions/pi-web-agent/src/backends/factory.ts +152 -0
- package/dist/extensions/pi-web-agent/src/backends/settings-reader.ts +25 -0
- package/dist/extensions/pi-web-agent/src/cache/ttl-cache.ts +28 -0
- package/dist/extensions/pi-web-agent/src/changelog-notice.ts +133 -0
- package/dist/extensions/pi-web-agent/src/commands/web-agent-config.ts +946 -0
- package/dist/extensions/pi-web-agent/src/extension.ts +126 -0
- package/dist/extensions/pi-web-agent/src/extract/readability.ts +118 -0
- package/dist/extensions/pi-web-agent/src/fetch/browser-resolution.ts +199 -0
- package/dist/extensions/pi-web-agent/src/fetch/firecrawl-fetch.ts +100 -0
- package/dist/extensions/pi-web-agent/src/fetch/headless-fetch.ts +117 -0
- package/dist/extensions/pi-web-agent/src/fetch/http-fetch.ts +67 -0
- package/dist/extensions/pi-web-agent/src/orchestration/answer-synthesizer.ts +60 -0
- package/dist/extensions/pi-web-agent/src/orchestration/candidate-selector.ts +50 -0
- package/dist/extensions/pi-web-agent/src/orchestration/direct-url.ts +52 -0
- package/dist/extensions/pi-web-agent/src/orchestration/evidence-quality.ts +105 -0
- package/dist/extensions/pi-web-agent/src/orchestration/evidence-ranker.ts +45 -0
- package/dist/extensions/pi-web-agent/src/orchestration/index.ts +28 -0
- package/dist/extensions/pi-web-agent/src/orchestration/query-planner.ts +47 -0
- package/dist/extensions/pi-web-agent/src/orchestration/research-orchestrator.ts +376 -0
- package/dist/extensions/pi-web-agent/src/orchestration/research-types.ts +64 -0
- package/dist/extensions/pi-web-agent/src/orchestration/research-worker.ts +181 -0
- package/dist/extensions/pi-web-agent/src/orchestration/source-profile.ts +101 -0
- package/dist/extensions/pi-web-agent/src/orchestration/stop-decider.ts +81 -0
- package/dist/extensions/pi-web-agent/src/presentation/config-store.ts +208 -0
- package/dist/extensions/pi-web-agent/src/presentation/config.ts +75 -0
- package/dist/extensions/pi-web-agent/src/presentation/explore-presentation.ts +61 -0
- package/dist/extensions/pi-web-agent/src/presentation/fetch-presentation.ts +54 -0
- package/dist/extensions/pi-web-agent/src/presentation/search-presentation.ts +41 -0
- package/dist/extensions/pi-web-agent/src/presentation/select-view.ts +20 -0
- package/dist/extensions/pi-web-agent/src/presentation/types.ts +63 -0
- package/dist/extensions/pi-web-agent/src/search/brave.ts +114 -0
- package/dist/extensions/pi-web-agent/src/search/duckduckgo.ts +72 -0
- package/dist/extensions/pi-web-agent/src/search/searxng.ts +96 -0
- package/dist/extensions/pi-web-agent/src/tools/web-explore.ts +71 -0
- package/dist/extensions/pi-web-agent/src/tools/web-fetch-headless.ts +31 -0
- package/dist/extensions/pi-web-agent/src/tools/web-fetch.ts +31 -0
- package/dist/extensions/pi-web-agent/src/tools/web-search.ts +157 -0
- package/dist/extensions/pi-web-agent/src/types.ts +88 -0
- package/dist/extensions/ponytail/index.js +189 -0
- package/dist/extensions/ponytail/package.json +8 -0
- package/dist/extensions/ponytail/ponytail-config.cjs +122 -0
- package/dist/extensions/ponytail/ponytail-instructions.cjs +94 -0
- package/dist/extensions/ponytail/test/extension.test.js +167 -0
- package/dist/extensions/ponytail/test/helpers.test.js +92 -0
- package/dist/extensions/question/constants.ts +38 -0
- package/dist/extensions/question/dialog-adapter.ts +74 -0
- package/dist/extensions/question/helpers.ts +102 -0
- package/dist/extensions/question/index.ts +671 -0
- package/dist/extensions/question/navigation.ts +14 -0
- package/dist/extensions/question/package.json +18 -0
- package/dist/extensions/question/question-list.ts +498 -0
- package/dist/extensions/question/row-layout.ts +162 -0
- package/dist/extensions/question/schemas.ts +69 -0
- package/dist/extensions/question/selection-mode.ts +49 -0
- package/dist/extensions/question/shortcuts.ts +53 -0
- package/dist/extensions/question/tests/helpers.test.ts +178 -0
- package/dist/extensions/question/tests/question-list.test.ts +230 -0
- package/dist/extensions/question/tests/row-layout.test.ts +136 -0
- package/dist/extensions/question/tests/shortcuts.test.ts +85 -0
- package/dist/extensions/question/tests/ui-protocol.test.ts +143 -0
- package/dist/extensions/question/tui-adapter.ts +33 -0
- package/dist/extensions/question/types.ts +102 -0
- package/dist/extensions/question/ui-protocol.ts +46 -0
- package/dist/extensions/tokenin-onboarding.ts +265 -2
- package/dist/extensions/web-agent-onboarding.ts +222 -0
- package/dist/extensions/workflow/adapter.ts +588 -0
- package/dist/extensions/workflow/extension.ts +24 -0
- package/dist/extensions/workflow/modes/prototype.ts +156 -0
- package/dist/extensions/workflow/modes/quick.ts +138 -0
- package/dist/extensions/workflow/package.json +17 -0
- package/dist/extensions/workflow/state-machine.ts +386 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +2 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +57 -13
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/utils/mime.d.ts.map +1 -1
- package/dist/utils/mime.js +24 -1
- package/dist/utils/mime.js.map +1 -1
- package/docs/docs.json +4 -0
- package/docs/workflows.md +206 -0
- package/package.json +5 -1
- package/README.md +0 -198
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { PresentationEnvelope } from './presentation/types.js';
|
|
2
|
+
|
|
3
|
+
export const TOOL_STATUSES = [
|
|
4
|
+
'ok',
|
|
5
|
+
'needs_headless',
|
|
6
|
+
'blocked',
|
|
7
|
+
'unsupported',
|
|
8
|
+
'error'
|
|
9
|
+
] as const;
|
|
10
|
+
|
|
11
|
+
export type ToolStatus = (typeof TOOL_STATUSES)[number];
|
|
12
|
+
|
|
13
|
+
export type SearchResult = {
|
|
14
|
+
title: string;
|
|
15
|
+
url: string;
|
|
16
|
+
snippet: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type ToolError = {
|
|
20
|
+
code: string;
|
|
21
|
+
message: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type SearchMetadata = {
|
|
25
|
+
backend: 'duckduckgo' | 'searxng' | 'brave';
|
|
26
|
+
cacheHit: boolean;
|
|
27
|
+
fallbackFrom?: 'searxng' | 'brave';
|
|
28
|
+
fallbackReason?: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type FetchMetadata = {
|
|
32
|
+
method: 'http' | 'headless' | 'firecrawl';
|
|
33
|
+
cacheHit: boolean;
|
|
34
|
+
fallbackFrom?: 'firecrawl';
|
|
35
|
+
fallbackReason?: string;
|
|
36
|
+
contentType?: string;
|
|
37
|
+
truncated?: boolean;
|
|
38
|
+
browser?: 'configured' | 'chrome' | 'edge' | 'brave' | 'chromium';
|
|
39
|
+
navigationMs?: number;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type ExtractedContent = {
|
|
43
|
+
title?: string;
|
|
44
|
+
byline?: string;
|
|
45
|
+
text: string;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type WebSearchResponse = {
|
|
49
|
+
status: 'ok' | 'error';
|
|
50
|
+
results: SearchResult[];
|
|
51
|
+
metadata: SearchMetadata;
|
|
52
|
+
presentation?: PresentationEnvelope;
|
|
53
|
+
error?: ToolError;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export type WebFetchResponse = {
|
|
57
|
+
status: ToolStatus;
|
|
58
|
+
url: string;
|
|
59
|
+
content?: ExtractedContent;
|
|
60
|
+
metadata: FetchMetadata;
|
|
61
|
+
presentation?: PresentationEnvelope;
|
|
62
|
+
error?: ToolError;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export type WebFetchHeadlessResponse = {
|
|
66
|
+
status: Exclude<ToolStatus, 'needs_headless'>;
|
|
67
|
+
url: string;
|
|
68
|
+
content?: ExtractedContent;
|
|
69
|
+
metadata: FetchMetadata;
|
|
70
|
+
presentation?: PresentationEnvelope;
|
|
71
|
+
error?: ToolError;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export type WebExploreResponse = {
|
|
75
|
+
status: 'ok' | 'error';
|
|
76
|
+
findings: string[];
|
|
77
|
+
sources: Array<{ title: string; url: string; method?: 'http' | 'headless' | 'firecrawl' }>;
|
|
78
|
+
caveat?: string;
|
|
79
|
+
metadata?: {
|
|
80
|
+
searchPasses: number;
|
|
81
|
+
fetchedPages: number;
|
|
82
|
+
headlessAttempts: number;
|
|
83
|
+
exhaustedBudget: boolean;
|
|
84
|
+
caveatReasons?: string[];
|
|
85
|
+
};
|
|
86
|
+
presentation?: PresentationEnvelope;
|
|
87
|
+
error?: ToolError;
|
|
88
|
+
};
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
|
|
3
|
+
const require = createRequire(import.meta.url);
|
|
4
|
+
const {
|
|
5
|
+
DEFAULT_MODE,
|
|
6
|
+
getDefaultMode,
|
|
7
|
+
normalizeMode,
|
|
8
|
+
normalizeConfigMode,
|
|
9
|
+
normalizePersistedMode,
|
|
10
|
+
isDeactivationCommand,
|
|
11
|
+
writeDefaultMode,
|
|
12
|
+
} = require("./ponytail-config.cjs");
|
|
13
|
+
const { getPonytailInstructions, filterSkillBodyForMode } = require("./ponytail-instructions.cjs");
|
|
14
|
+
|
|
15
|
+
export { filterSkillBodyForMode };
|
|
16
|
+
export const readDefaultMode = getDefaultMode;
|
|
17
|
+
|
|
18
|
+
export function resolveSessionMode(entries, fallbackMode = DEFAULT_MODE) {
|
|
19
|
+
const fallback = normalizePersistedMode(fallbackMode) || DEFAULT_MODE;
|
|
20
|
+
if (!Array.isArray(entries)) return fallback;
|
|
21
|
+
|
|
22
|
+
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
23
|
+
const entry = entries[i];
|
|
24
|
+
if (entry?.type !== "custom" || entry?.customType !== "ponytail-mode") continue;
|
|
25
|
+
|
|
26
|
+
const mode = normalizePersistedMode(entry?.data?.mode);
|
|
27
|
+
if (mode) return mode;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return fallback;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function parsePonytailCommand(text, defaultMode = DEFAULT_MODE) {
|
|
34
|
+
const fallback = normalizePersistedMode(defaultMode) || DEFAULT_MODE;
|
|
35
|
+
const normalizedText = String(text || "").trim().toLowerCase();
|
|
36
|
+
|
|
37
|
+
if (!normalizedText) {
|
|
38
|
+
return { type: "set-mode", mode: fallback === "off" ? "full" : fallback };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const [primary, secondary] = normalizedText.split(/\s+/);
|
|
42
|
+
|
|
43
|
+
if (primary === "status") return { type: "status" };
|
|
44
|
+
|
|
45
|
+
if (primary === "default") {
|
|
46
|
+
const mode = normalizeConfigMode(secondary);
|
|
47
|
+
return mode ? { type: "set-default", mode } : { type: "invalid", reason: "invalid-default-mode" };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const mode = normalizeMode(primary);
|
|
51
|
+
return mode ? { type: "set-mode", mode } : { type: "invalid", reason: "invalid-mode", mode: primary };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export { writeDefaultMode };
|
|
55
|
+
|
|
56
|
+
export default function ponytailExtension(pi) {
|
|
57
|
+
let currentMode = DEFAULT_MODE;
|
|
58
|
+
let configuredDefaultMode = getDefaultMode();
|
|
59
|
+
let isActive = false;
|
|
60
|
+
let lastCtx = null;
|
|
61
|
+
|
|
62
|
+
// -- Status bar --
|
|
63
|
+
function syncStatus(ctx) {
|
|
64
|
+
if (ctx) lastCtx = ctx;
|
|
65
|
+
const c = ctx || lastCtx;
|
|
66
|
+
if (!c?.ui?.setStatus || !c.ui.theme?.fg) return;
|
|
67
|
+
const theme = c.ui.theme;
|
|
68
|
+
if (currentMode === "off") {
|
|
69
|
+
c.ui.setStatus("ponytail", "");
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const levelIcons = { lite: "🌿", full: "⚡", ultra: "🔥" };
|
|
73
|
+
const icon = levelIcons[currentMode] || "";
|
|
74
|
+
const label = currentMode.toUpperCase();
|
|
75
|
+
const indicator = isActive ? theme.fg("accent", "●") : theme.fg("dim", "○");
|
|
76
|
+
c.ui.setStatus("ponytail", indicator + " 🐴 " + theme.fg("muted", "ponytail: ") + theme.fg("text", icon + " " + label));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const setMode = (mode, ctx) => {
|
|
80
|
+
const normalized = normalizePersistedMode(mode);
|
|
81
|
+
if (!normalized) return;
|
|
82
|
+
|
|
83
|
+
currentMode = normalized;
|
|
84
|
+
pi.appendEntry("ponytail-mode", { mode: normalized });
|
|
85
|
+
syncStatus(ctx);
|
|
86
|
+
ctx?.ui?.notify?.(`Ponytail mode set to ${normalized}.`, "info");
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const sendAlias = (skillName, args, ctx) => {
|
|
90
|
+
const normalized = String(args || "").trim();
|
|
91
|
+
const message = normalized ? `${skillName} ${normalized}` : skillName;
|
|
92
|
+
|
|
93
|
+
if (ctx?.isIdle?.() === false) {
|
|
94
|
+
pi.sendUserMessage(message, { deliverAs: "followUp" });
|
|
95
|
+
ctx?.ui?.notify?.(`${skillName} queued as follow-up.`, "info");
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
pi.sendUserMessage(message);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
pi.registerCommand("ponytail", {
|
|
103
|
+
description: "Set or report Ponytail mode",
|
|
104
|
+
handler: async (args, ctx) => {
|
|
105
|
+
const parsed = parsePonytailCommand(args, configuredDefaultMode);
|
|
106
|
+
|
|
107
|
+
if (parsed.type === "status") {
|
|
108
|
+
ctx?.ui?.notify?.(`Ponytail: current ${currentMode} • default ${configuredDefaultMode}`, "info");
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (parsed.type === "set-default") {
|
|
113
|
+
const written = writeDefaultMode(parsed.mode);
|
|
114
|
+
if (written) {
|
|
115
|
+
configuredDefaultMode = getDefaultMode();
|
|
116
|
+
const message = configuredDefaultMode === written
|
|
117
|
+
? `Default Ponytail mode set to ${written}.`
|
|
118
|
+
: `Saved default ${written}, but env override keeps default at ${configuredDefaultMode}.`;
|
|
119
|
+
ctx?.ui?.notify?.(message, "info");
|
|
120
|
+
}
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (parsed.type === "set-mode") {
|
|
125
|
+
setMode(parsed.mode, ctx);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
ctx?.ui?.notify?.("Unknown or unsupported /ponytail mode.", "warning");
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
pi.registerCommand("ponytail-review", {
|
|
134
|
+
description: "Run /skill:ponytail-review",
|
|
135
|
+
handler: (_args, ctx) => sendAlias("/skill:ponytail-review", "", ctx),
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
pi.registerCommand("ponytail-audit", {
|
|
139
|
+
description: "Run /skill:ponytail-audit",
|
|
140
|
+
handler: (_args, ctx) => sendAlias("/skill:ponytail-audit", "", ctx),
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
pi.registerCommand("ponytail-gain", {
|
|
144
|
+
description: "Run /skill:ponytail-gain",
|
|
145
|
+
handler: (_args, ctx) => sendAlias("/skill:ponytail-gain", "", ctx),
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
pi.registerCommand("ponytail-debt", {
|
|
149
|
+
description: "Run /skill:ponytail-debt",
|
|
150
|
+
handler: (_args, ctx) => sendAlias("/skill:ponytail-debt", "", ctx),
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
pi.registerCommand("ponytail-help", {
|
|
154
|
+
description: "Run /skill:ponytail-help",
|
|
155
|
+
handler: (_args, ctx) => sendAlias("/skill:ponytail-help", "", ctx),
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
pi.on("input", async (event) => {
|
|
159
|
+
if (event?.source === "extension") return;
|
|
160
|
+
|
|
161
|
+
const text = String(event?.text || "");
|
|
162
|
+
if (currentMode !== "off" && isDeactivationCommand(text)) {
|
|
163
|
+
setMode("off");
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
168
|
+
const entries = ctx?.sessionManager?.getBranch?.() || ctx?.sessionManager?.getEntries?.() || [];
|
|
169
|
+
configuredDefaultMode = getDefaultMode();
|
|
170
|
+
currentMode = resolveSessionMode(entries, configuredDefaultMode);
|
|
171
|
+
syncStatus(ctx);
|
|
172
|
+
ctx?.ui?.notify?.(`Ponytail loaded: ${currentMode}`, "info");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
pi.on("agent_start", async (_event, ctx) => {
|
|
176
|
+
isActive = true;
|
|
177
|
+
syncStatus(ctx);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
pi.on("agent_end", async (_event, ctx) => {
|
|
181
|
+
isActive = false;
|
|
182
|
+
syncStatus(ctx);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
pi.on("before_agent_start", async (event) => {
|
|
186
|
+
if (!currentMode || currentMode === "off") return;
|
|
187
|
+
return { systemPrompt: `${event.systemPrompt}\n\n${getPonytailInstructions(currentMode)}` };
|
|
188
|
+
});
|
|
189
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ponytail — shared configuration resolver
|
|
3
|
+
//
|
|
4
|
+
// Resolution order for default mode:
|
|
5
|
+
// 1. PONYTAIL_DEFAULT_MODE environment variable
|
|
6
|
+
// 2. Config file defaultMode field:
|
|
7
|
+
// - $XDG_CONFIG_HOME/ponytail/config.json (any platform, if set)
|
|
8
|
+
// - ~/.config/ponytail/config.json (macOS / Linux fallback)
|
|
9
|
+
// - %APPDATA%\ponytail\config.json (Windows fallback)
|
|
10
|
+
// 3. 'full'
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const os = require('os');
|
|
15
|
+
|
|
16
|
+
const DEFAULT_MODE = 'full';
|
|
17
|
+
const VALID_MODES = ['off', 'lite', 'full', 'ultra', 'review'];
|
|
18
|
+
const RUNTIME_MODES = ['off', 'lite', 'full', 'ultra'];
|
|
19
|
+
|
|
20
|
+
function normalizeMode(mode) {
|
|
21
|
+
if (typeof mode !== 'string') return null;
|
|
22
|
+
const normalized = mode.trim().toLowerCase();
|
|
23
|
+
return RUNTIME_MODES.includes(normalized) ? normalized : null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function normalizeConfigMode(mode) {
|
|
27
|
+
if (typeof mode !== 'string') return null;
|
|
28
|
+
const normalized = mode.trim().toLowerCase();
|
|
29
|
+
return VALID_MODES.includes(normalized) ? normalized : null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function normalizePersistedMode(mode) {
|
|
33
|
+
return normalizeMode(mode) || normalizeConfigMode(mode);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// "stop ponytail" / "normal mode" turn ponytail off, but only as a standalone
|
|
37
|
+
// command. Matching the phrase anywhere in the message turned it off mid-task
|
|
38
|
+
// for ordinary requests like "add a normal mode toggle" — so require the whole
|
|
39
|
+
// message to be the command, ignoring case and trailing punctuation.
|
|
40
|
+
function isDeactivationCommand(text) {
|
|
41
|
+
const t = String(text || '').trim().toLowerCase().replace(/[.!?\s]+$/, '');
|
|
42
|
+
return t === 'stop ponytail' || t === 'normal mode';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ponytail: only embed the plugin install path in a statusline shell command when
|
|
46
|
+
// it's made of ordinary path characters. An allowlist beats escaping every shell's
|
|
47
|
+
// metacharacters; a hostile clone path (quotes, &, $, backtick, ;, etc.) falls back
|
|
48
|
+
// to manual setup instead. Allows : \ / for normal Windows and POSIX paths. Full
|
|
49
|
+
// per-shell escaper only if a real need appears.
|
|
50
|
+
function isShellSafe(p) {
|
|
51
|
+
return typeof p === 'string' && /^[A-Za-z0-9 _.\-:/\\~]+$/.test(p);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getConfigDir() {
|
|
55
|
+
if (process.env.XDG_CONFIG_HOME) {
|
|
56
|
+
return path.join(process.env.XDG_CONFIG_HOME, 'ponytail');
|
|
57
|
+
}
|
|
58
|
+
if (process.platform === 'win32') {
|
|
59
|
+
return path.join(
|
|
60
|
+
process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'),
|
|
61
|
+
'ponytail'
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
return path.join(os.homedir(), '.config', 'ponytail');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function getConfigPath() {
|
|
68
|
+
return path.join(getConfigDir(), 'config.json');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function getClaudeDir() {
|
|
72
|
+
// ponytail: CLAUDE_CONFIG_DIR overrides ~/.claude, matching Claude Code.
|
|
73
|
+
return process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function getDefaultMode() {
|
|
77
|
+
// 1. Environment variable (highest priority)
|
|
78
|
+
const envMode = process.env.PONYTAIL_DEFAULT_MODE;
|
|
79
|
+
if (envMode && VALID_MODES.includes(envMode.toLowerCase())) {
|
|
80
|
+
return envMode.toLowerCase();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 2. Config file
|
|
84
|
+
try {
|
|
85
|
+
const configPath = getConfigPath();
|
|
86
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
87
|
+
if (config.defaultMode && VALID_MODES.includes(config.defaultMode.toLowerCase())) {
|
|
88
|
+
return config.defaultMode.toLowerCase();
|
|
89
|
+
}
|
|
90
|
+
} catch (e) {
|
|
91
|
+
// Config file doesn't exist or is invalid — fall through
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 3. Default
|
|
95
|
+
return DEFAULT_MODE;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function writeDefaultMode(mode) {
|
|
99
|
+
const normalized = normalizeConfigMode(mode);
|
|
100
|
+
if (!normalized) return null;
|
|
101
|
+
|
|
102
|
+
const configPath = getConfigPath();
|
|
103
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
104
|
+
fs.writeFileSync(configPath, JSON.stringify({ defaultMode: normalized }, null, 2), 'utf8');
|
|
105
|
+
return normalized;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
module.exports = {
|
|
109
|
+
DEFAULT_MODE,
|
|
110
|
+
VALID_MODES,
|
|
111
|
+
RUNTIME_MODES,
|
|
112
|
+
getDefaultMode,
|
|
113
|
+
getConfigDir,
|
|
114
|
+
getConfigPath,
|
|
115
|
+
getClaudeDir,
|
|
116
|
+
isShellSafe,
|
|
117
|
+
normalizeMode,
|
|
118
|
+
normalizeConfigMode,
|
|
119
|
+
normalizePersistedMode,
|
|
120
|
+
isDeactivationCommand,
|
|
121
|
+
writeDefaultMode,
|
|
122
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Shared Ponytail instruction builder for Claude hooks and Pi extension.
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { DEFAULT_MODE, normalizeMode, normalizePersistedMode } = require('./ponytail-config.cjs');
|
|
7
|
+
|
|
8
|
+
const INDEPENDENT_MODES = new Set(['review']);
|
|
9
|
+
const SKILL_PATH = path.join(__dirname, '..', '..', 'skills', 'ponytail', 'SKILL.md');
|
|
10
|
+
|
|
11
|
+
function filterSkillBodyForMode(body, mode) {
|
|
12
|
+
const effectiveMode = normalizeMode(mode) || DEFAULT_MODE;
|
|
13
|
+
const withoutFrontmatter = String(body || '').replace(/^---[\s\S]*?---\s*/, '');
|
|
14
|
+
|
|
15
|
+
// Only the intensity table rows and worked examples are mode-specific, and
|
|
16
|
+
// both are keyed by a mode name (lite/full/ultra). A bullet whose label is
|
|
17
|
+
// not a mode — e.g. "No unrequested abstractions: ..." — is a normal rule
|
|
18
|
+
// and must be kept verbatim.
|
|
19
|
+
return withoutFrontmatter
|
|
20
|
+
.split(/\r?\n/)
|
|
21
|
+
.filter((line) => {
|
|
22
|
+
const tableLabel = line.match(/^\|\s*\*\*(.+?)\*\*\s*\|/);
|
|
23
|
+
if (tableLabel) {
|
|
24
|
+
const labelMode = normalizeMode(tableLabel[1].trim());
|
|
25
|
+
if (labelMode) return labelMode === effectiveMode;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const exampleLabel = line.match(/^-\s*([^:]+):\s*/);
|
|
29
|
+
if (exampleLabel) {
|
|
30
|
+
const labelMode = normalizeMode(exampleLabel[1].trim());
|
|
31
|
+
if (labelMode) return labelMode === effectiveMode;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return true;
|
|
35
|
+
})
|
|
36
|
+
.join('\n');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getFallbackInstructions(mode) {
|
|
40
|
+
return 'PONYTAIL MODE ACTIVE — level: ' + mode + '\n\n' +
|
|
41
|
+
'You are a lazy senior developer. Lazy means efficient, not careless. The best code is the code never written.\n\n' +
|
|
42
|
+
'## Persistence\n\n' +
|
|
43
|
+
'ACTIVE EVERY RESPONSE. No drift back to over-building. Still active if unsure. Off only: "stop ponytail" / "normal mode".\n\n' +
|
|
44
|
+
'Current level: **' + mode + '**. Switch: `/ponytail lite|full|ultra`.\n\n' +
|
|
45
|
+
'## The ladder\n\n' +
|
|
46
|
+
'Before any code, stop at the first rung that holds (the ladder runs after you understand the problem, not instead of it — read the code it touches and trace the real flow first):\n' +
|
|
47
|
+
'1. Does this need to be built at all? (YAGNI)\n' +
|
|
48
|
+
'2. Does it already exist in this codebase? Reuse what is already here, do not re-write it.\n' +
|
|
49
|
+
'3. Does the standard library do this? Use it.\n' +
|
|
50
|
+
'4. Does a native platform feature cover it? Use it.\n' +
|
|
51
|
+
'5. Does an already-installed dependency solve it? Use it.\n' +
|
|
52
|
+
'6. Can this be one line? Make it one line.\n' +
|
|
53
|
+
'7. Only then: write the minimum code that works.\n\n' +
|
|
54
|
+
'Bug fix = root cause, not symptom: grep every caller of the function you touch and fix the shared function once (a smaller diff than one guard per caller); patching only the path the ticket names leaves a sibling caller broken.\n\n' +
|
|
55
|
+
'## Rules\n\n' +
|
|
56
|
+
'No abstractions that were not requested. No avoidable dependencies. No boilerplate nobody asked for. ' +
|
|
57
|
+
'Deletion over addition. Boring over clever. Fewest files possible. ' +
|
|
58
|
+
'Ship the lazy version and question the complex request in the same response — never stall. ' +
|
|
59
|
+
'Between two same-size stdlib options, pick the one correct on edge cases. ' +
|
|
60
|
+
'Mark intentional simplifications with a `ponytail:` comment — a shortcut with a known ceiling names the ceiling and the upgrade path in the comment.\n\n' +
|
|
61
|
+
'## Output\n\n' +
|
|
62
|
+
'Code first. Then at most three short lines: what was skipped, when to add it. ' +
|
|
63
|
+
'If the explanation is longer than the code, delete the explanation. ' +
|
|
64
|
+
'Explanation the user explicitly asked for is not debt, give it in full.\n\n' +
|
|
65
|
+
'## When NOT to be lazy\n\n' +
|
|
66
|
+
'Never simplify away: understanding the problem (read it fully and trace the real flow before picking a rung — a small diff you do not understand is just laziness dressed up as efficiency), input validation at trust boundaries, error handling that prevents data loss, ' +
|
|
67
|
+
'security measures, accessibility basics, the calibration real hardware needs (the platform is never the spec ideal), anything the user explicitly asked to keep. ' +
|
|
68
|
+
'Lazy code without its check is unfinished: non-trivial logic leaves ONE runnable check behind (assert-based demo/self-check or one small test file; no frameworks). Trivial one-liners need no test.\n\n' +
|
|
69
|
+
'## Boundaries\n\n' +
|
|
70
|
+
'Ponytail governs what you build, not how you talk. "stop ponytail" or "normal mode": revert. Level persists until changed or session end.';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function getPonytailInstructions(mode) {
|
|
74
|
+
const configuredMode = normalizePersistedMode(mode) || DEFAULT_MODE;
|
|
75
|
+
|
|
76
|
+
if (INDEPENDENT_MODES.has(configuredMode)) {
|
|
77
|
+
return 'PONYTAIL MODE ACTIVE — level: ' + configuredMode + '. Behavior defined by /ponytail-' + configuredMode + ' skill.';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const effectiveMode = normalizeMode(configuredMode) || DEFAULT_MODE;
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
return 'PONYTAIL MODE ACTIVE — level: ' + effectiveMode + '\n\n' +
|
|
84
|
+
filterSkillBodyForMode(fs.readFileSync(SKILL_PATH, 'utf8'), effectiveMode);
|
|
85
|
+
} catch (e) {
|
|
86
|
+
return getFallbackInstructions(effectiveMode);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = {
|
|
91
|
+
filterSkillBodyForMode,
|
|
92
|
+
getFallbackInstructions,
|
|
93
|
+
getPonytailInstructions,
|
|
94
|
+
};
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
|
|
7
|
+
import ponytailExtension from "../index.js";
|
|
8
|
+
|
|
9
|
+
function createPiHarness() {
|
|
10
|
+
const events = new Map();
|
|
11
|
+
const commands = new Map();
|
|
12
|
+
const appendedEntries = [];
|
|
13
|
+
const sentUserMessages = [];
|
|
14
|
+
|
|
15
|
+
const pi = {
|
|
16
|
+
on(eventName, handler) {
|
|
17
|
+
events.set(eventName, handler);
|
|
18
|
+
},
|
|
19
|
+
registerCommand(name, options) {
|
|
20
|
+
commands.set(name, options);
|
|
21
|
+
},
|
|
22
|
+
appendEntry(customType, data) {
|
|
23
|
+
appendedEntries.push({ customType, data });
|
|
24
|
+
},
|
|
25
|
+
sendUserMessage(text, options) {
|
|
26
|
+
sentUserMessages.push({ text, options });
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
ponytailExtension(pi);
|
|
31
|
+
return { events, commands, appendedEntries, sentUserMessages };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function createCommandContext(overrides = {}) {
|
|
35
|
+
return {
|
|
36
|
+
isIdle: () => true,
|
|
37
|
+
sessionManager: { getEntries: () => [] },
|
|
38
|
+
ui: { notify() {} },
|
|
39
|
+
...overrides,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function withTempConfig(fn) {
|
|
44
|
+
const tempConfigHome = mkdtempSync(join(tmpdir(), "ponytail-test-"));
|
|
45
|
+
const previousXdg = process.env.XDG_CONFIG_HOME;
|
|
46
|
+
process.env.XDG_CONFIG_HOME = tempConfigHome;
|
|
47
|
+
|
|
48
|
+
return Promise.resolve()
|
|
49
|
+
.then(fn)
|
|
50
|
+
.finally(() => {
|
|
51
|
+
if (previousXdg === undefined) delete process.env.XDG_CONFIG_HOME;
|
|
52
|
+
else process.env.XDG_CONFIG_HOME = previousXdg;
|
|
53
|
+
rmSync(tempConfigHome, { recursive: true, force: true });
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
test("extension registers Ponytail commands", () => {
|
|
58
|
+
const { commands } = createPiHarness();
|
|
59
|
+
|
|
60
|
+
assert.deepEqual([...commands.keys()].sort(), ["ponytail", "ponytail-audit", "ponytail-debt", "ponytail-gain", "ponytail-help", "ponytail-review"]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("/ponytail updates session mode and injects instructions", async () => withTempConfig(async () => {
|
|
64
|
+
const { commands, events, appendedEntries } = createPiHarness();
|
|
65
|
+
const ctx = createCommandContext();
|
|
66
|
+
|
|
67
|
+
await events.get("session_start")({ reason: "startup" }, ctx);
|
|
68
|
+
await commands.get("ponytail").handler("ultra", ctx);
|
|
69
|
+
|
|
70
|
+
assert.deepEqual(appendedEntries.at(-1), {
|
|
71
|
+
customType: "ponytail-mode",
|
|
72
|
+
data: { mode: "ultra" },
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const result = await events.get("before_agent_start")({ systemPrompt: "BASE" }, ctx);
|
|
76
|
+
assert.ok(result.systemPrompt.includes("PONYTAIL MODE ACTIVE"));
|
|
77
|
+
assert.ok(result.systemPrompt.includes("ultra"));
|
|
78
|
+
}));
|
|
79
|
+
|
|
80
|
+
test("session_start restores latest persisted mode", async () => withTempConfig(async () => {
|
|
81
|
+
const { events } = createPiHarness();
|
|
82
|
+
const ctx = createCommandContext({
|
|
83
|
+
sessionManager: {
|
|
84
|
+
getEntries: () => [
|
|
85
|
+
{ type: "custom", customType: "ponytail-mode", data: { mode: "lite" } },
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
await events.get("session_start")({ reason: "resume" }, ctx);
|
|
91
|
+
const result = await events.get("before_agent_start")({ systemPrompt: "BASE" }, ctx);
|
|
92
|
+
|
|
93
|
+
assert.ok(result.systemPrompt.includes("lite"));
|
|
94
|
+
}));
|
|
95
|
+
|
|
96
|
+
test("skill alias commands delegate to Pi skill commands", async () => {
|
|
97
|
+
const { commands, sentUserMessages } = createPiHarness();
|
|
98
|
+
const ctx = createCommandContext();
|
|
99
|
+
|
|
100
|
+
await commands.get("ponytail-review").handler("", ctx);
|
|
101
|
+
await commands.get("ponytail-audit").handler("", ctx);
|
|
102
|
+
await commands.get("ponytail-debt").handler("", ctx);
|
|
103
|
+
await commands.get("ponytail-gain").handler("", ctx);
|
|
104
|
+
await commands.get("ponytail-help").handler("", ctx);
|
|
105
|
+
|
|
106
|
+
assert.deepEqual(sentUserMessages.map((entry) => entry.text), [
|
|
107
|
+
"/skill:ponytail-review",
|
|
108
|
+
"/skill:ponytail-audit",
|
|
109
|
+
"/skill:ponytail-debt",
|
|
110
|
+
"/skill:ponytail-gain",
|
|
111
|
+
"/skill:ponytail-help",
|
|
112
|
+
]);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("normal mode disables persistent instructions", async () => withTempConfig(async () => {
|
|
116
|
+
const { commands, events } = createPiHarness();
|
|
117
|
+
const ctx = createCommandContext();
|
|
118
|
+
|
|
119
|
+
await events.get("session_start")({ reason: "startup" }, ctx);
|
|
120
|
+
await commands.get("ponytail").handler("ultra", ctx);
|
|
121
|
+
await events.get("input")({ text: "normal mode", source: "interactive" }, ctx);
|
|
122
|
+
|
|
123
|
+
const disabled = await events.get("before_agent_start")({ systemPrompt: "BASE" }, ctx);
|
|
124
|
+
assert.equal(disabled, undefined);
|
|
125
|
+
}));
|
|
126
|
+
|
|
127
|
+
test("a request mentioning normal mode stays active", async () => withTempConfig(async () => {
|
|
128
|
+
const { commands, events } = createPiHarness();
|
|
129
|
+
const ctx = createCommandContext();
|
|
130
|
+
|
|
131
|
+
await events.get("session_start")({ reason: "startup" }, ctx);
|
|
132
|
+
await commands.get("ponytail").handler("ultra", ctx);
|
|
133
|
+
await events.get("input")({ text: "add a normal mode toggle next to dark mode", source: "interactive" }, ctx);
|
|
134
|
+
|
|
135
|
+
const result = await events.get("before_agent_start")({ systemPrompt: "BASE" }, ctx);
|
|
136
|
+
assert.match(result.systemPrompt, /PONYTAIL MODE ACTIVE/);
|
|
137
|
+
}));
|
|
138
|
+
|
|
139
|
+
test("status bar renders the mode and flips active on agent_start", async () => withTempConfig(async () => {
|
|
140
|
+
const { events } = createPiHarness();
|
|
141
|
+
const statusWrites = [];
|
|
142
|
+
const ctx = createCommandContext({
|
|
143
|
+
sessionManager: { getEntries: () => [{ type: "custom", customType: "ponytail-mode", data: { mode: "ultra" } }] },
|
|
144
|
+
ui: { notify() {}, setStatus: (key, text) => statusWrites.push({ key, text }), theme: { fg: (_color, text) => text } },
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
await events.get("session_start")({ reason: "resume" }, ctx);
|
|
148
|
+
await events.get("agent_start")({}, ctx);
|
|
149
|
+
|
|
150
|
+
assert.equal(statusWrites.at(-2).key, "ponytail");
|
|
151
|
+
assert.match(statusWrites.at(-2).text, /○.*ULTRA/);
|
|
152
|
+
assert.match(statusWrites.at(-1).text, /●.*ULTRA/);
|
|
153
|
+
}));
|
|
154
|
+
|
|
155
|
+
test("status bar stays silent when ui lacks a theme", async () => withTempConfig(async () => {
|
|
156
|
+
const { events } = createPiHarness();
|
|
157
|
+
const calls = [];
|
|
158
|
+
const ctx = createCommandContext({
|
|
159
|
+
sessionManager: { getEntries: () => [{ type: "custom", customType: "ponytail-mode", data: { mode: "ultra" } }] },
|
|
160
|
+
ui: { notify() {}, setStatus: (_key, text) => calls.push(text) }, // setStatus present, theme absent
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
await events.get("session_start")({ reason: "resume" }, ctx);
|
|
164
|
+
await events.get("agent_start")({}, ctx);
|
|
165
|
+
|
|
166
|
+
assert.deepEqual(calls, []);
|
|
167
|
+
}));
|