@groeponline/pi-wishcraft 0.17.3
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/AGENTS.md +68 -0
- package/CHANGELOG.md +724 -0
- package/CONTRIBUTING.md +37 -0
- package/README.md +648 -0
- package/RELEASE.md +117 -0
- package/ROADMAP.md +52 -0
- package/bash-mode/completion-providers.ts +269 -0
- package/bash-mode/completion.ts +416 -0
- package/bash-mode/editor-ghost.ts +40 -0
- package/bash-mode/editor-input.ts +80 -0
- package/bash-mode/editor.ts +437 -0
- package/bash-mode/history.ts +263 -0
- package/bash-mode/shell-session.ts +286 -0
- package/bash-mode/transcript.ts +108 -0
- package/bash-mode/types.ts +80 -0
- package/index.ts +6 -0
- package/package.json +55 -0
- package/queue/store.ts +443 -0
- package/queue/types.ts +54 -0
- package/src/config/custom-items.ts +182 -0
- package/src/config/extension-statuses.ts +51 -0
- package/src/config/layout.ts +60 -0
- package/src/config/parse.ts +127 -0
- package/src/config/powerline-config.ts +18 -0
- package/src/config/presets.ts +245 -0
- package/src/config/primitives.ts +117 -0
- package/src/config/segment-ids.ts +114 -0
- package/src/config/segment-options.ts +128 -0
- package/src/config/settings-patch.ts +26 -0
- package/src/config/types.ts +277 -0
- package/src/core/frontmatter.ts +40 -0
- package/src/editor/autocomplete-chain.ts +41 -0
- package/src/extension/activate.ts +28 -0
- package/src/extension/bash-mode-actions.ts +104 -0
- package/src/extension/commands.ts +268 -0
- package/src/extension/constants.ts +46 -0
- package/src/extension/custom-editor.ts +406 -0
- package/src/extension/git-invalidation.ts +40 -0
- package/src/extension/layout.ts +160 -0
- package/src/extension/menu-views.ts +393 -0
- package/src/extension/powerline-widgets.ts +95 -0
- package/src/extension/prompt-history.ts +219 -0
- package/src/extension/queue-commands.ts +245 -0
- package/src/extension/queue-context.ts +12 -0
- package/src/extension/queue-integration.ts +434 -0
- package/src/extension/segment-context.ts +212 -0
- package/src/extension/session-lifecycle.ts +373 -0
- package/src/extension/settings-io.ts +202 -0
- package/src/extension/shortcuts-config.ts +357 -0
- package/src/extension/shortcuts-router.ts +383 -0
- package/src/extension/skills/inline-invocation.ts +174 -0
- package/src/extension/skills/ook.md +6 -0
- package/src/extension/skills/test.md +6 -0
- package/src/extension/stale-context.ts +10 -0
- package/src/extension/stash-history.ts +103 -0
- package/src/extension/state.ts +159 -0
- package/src/extension/status-line-renderers.ts +222 -0
- package/src/extension/types.ts +97 -0
- package/src/extension/vibe-command.ts +160 -0
- package/src/extension/welcome-control.ts +27 -0
- package/src/extension/welcome-integration.ts +153 -0
- package/src/git/status.ts +332 -0
- package/src/paths/agent-dirs.ts +67 -0
- package/src/render/timer.ts +46 -0
- package/src/segments/core.ts +256 -0
- package/src/segments/custom.ts +114 -0
- package/src/segments/index.ts +3 -0
- package/src/segments/registry.ts +87 -0
- package/src/segments/shared.ts +36 -0
- package/src/segments/system.ts +235 -0
- package/src/segments/usage.ts +178 -0
- package/src/shell/cd-command.ts +190 -0
- package/src/shortcuts/matching.ts +61 -0
- package/src/theme/colors.ts +60 -0
- package/src/theme/icons.ts +175 -0
- package/src/theme/separators.ts +41 -0
- package/src/theme/theme.ts +211 -0
- package/src/tools/graph.ts +75 -0
- package/src/tools/patch.ts +179 -0
- package/src/tools/ripgrep.ts +104 -0
- package/src/usage/context.ts +97 -0
- package/src/usage/ledger.ts +293 -0
- package/src/usage/rates.ts +155 -0
- package/src/welcome/auto-dismiss.ts +43 -0
- package/src/welcome/banner.ts +68 -0
- package/src/welcome/discover.ts +234 -0
- package/src/welcome/format.ts +18 -0
- package/src/welcome/index.ts +5 -0
- package/src/welcome/layout.ts +36 -0
- package/src/welcome/overlay.ts +80 -0
- package/src/welcome/renderer.ts +157 -0
- package/src/welcome/sessions.ts +107 -0
- package/src/welcome/types.ts +41 -0
- package/src/welcome/widgets/graph-widget.ts +25 -0
- package/src/welcome/widgets/index.ts +20 -0
- package/src/welcome/widgets/queue-widget.ts +26 -0
- package/src/welcome/widgets/sessions-widget.ts +23 -0
- package/src/welcome/widgets/shortcuts-widget.ts +17 -0
- package/src/welcome/widgets/system-widget.ts +29 -0
- package/src/working-vibes/generate.ts +144 -0
- package/src/working-vibes/index.ts +24 -0
- package/src/working-vibes/manager.ts +198 -0
- package/src/working-vibes/provider.ts +163 -0
- package/src/working-vibes/storage.ts +357 -0
- package/theme.example.json +24 -0
- package/tsconfig.json +13 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import type { PowerlineConfig } from "../config/powerline-config.ts";
|
|
2
|
+
import type { StatusLinePreset } from "../config/types.ts";
|
|
3
|
+
import { PRESETS } from "../config/presets.ts";
|
|
4
|
+
import { BashTranscriptStore } from "../../bash-mode/transcript.ts";
|
|
5
|
+
import { BashCompletionEngine } from "../../bash-mode/completion.ts";
|
|
6
|
+
import { ManagedShellSession } from "../../bash-mode/shell-session.ts";
|
|
7
|
+
import { PowerlineQueueStore } from "../../queue/store.ts";
|
|
8
|
+
import {
|
|
9
|
+
SessionBranchCache,
|
|
10
|
+
SessionTokenStatsCache,
|
|
11
|
+
} from "../usage/ledger.ts";
|
|
12
|
+
import { CoreContextUsageCache } from "../usage/context.ts";
|
|
13
|
+
import { createWelcomeDismissScheduler } from "../welcome/auto-dismiss.ts";
|
|
14
|
+
import { createRenderScheduler } from "../render/timer.ts";
|
|
15
|
+
import {
|
|
16
|
+
resolveShortcutConfig,
|
|
17
|
+
parseBashModeSettings,
|
|
18
|
+
} from "./shortcuts-config.ts";
|
|
19
|
+
import { readPersistedStashHistory } from "./stash-history.ts";
|
|
20
|
+
import { dismissWelcome } from "./welcome-control.ts";
|
|
21
|
+
import {
|
|
22
|
+
EDITOR_STATUS_DEFER_MS,
|
|
23
|
+
STATUS_RENDER_DEBOUNCE_MS,
|
|
24
|
+
} from "./constants.ts";
|
|
25
|
+
import type { RuntimeState } from "./types.ts";
|
|
26
|
+
|
|
27
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
28
|
+
// Configuration
|
|
29
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
30
|
+
|
|
31
|
+
export let config: PowerlineConfig = {
|
|
32
|
+
preset: "default",
|
|
33
|
+
customItems: [],
|
|
34
|
+
disabledSegments: [],
|
|
35
|
+
invalidDisabledSegments: [],
|
|
36
|
+
layout: null,
|
|
37
|
+
invalidLayoutSegments: [],
|
|
38
|
+
separator: null,
|
|
39
|
+
segmentOptions: {},
|
|
40
|
+
placement: "above",
|
|
41
|
+
invalidPlacement: null,
|
|
42
|
+
welcome: true,
|
|
43
|
+
stashSharpSShortcut: false,
|
|
44
|
+
queue: { captureSigil: "#" },
|
|
45
|
+
segments: {},
|
|
46
|
+
presets: {},
|
|
47
|
+
segmentLabels: {},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export function setConfig(next: PowerlineConfig): void {
|
|
51
|
+
config = next;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export let customCompactionEnabled = false;
|
|
55
|
+
|
|
56
|
+
export function setCustomCompactionEnabled(next: boolean): void {
|
|
57
|
+
customCompactionEnabled = next;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const PRESET_NAMES = Object.keys(PRESETS) as StatusLinePreset[];
|
|
61
|
+
|
|
62
|
+
export function isValidPreset(value: unknown): value is StatusLinePreset {
|
|
63
|
+
return (
|
|
64
|
+
typeof value === "string" &&
|
|
65
|
+
Object.prototype.hasOwnProperty.call(PRESETS, value)
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function normalizePreset(value: unknown): StatusLinePreset | null {
|
|
70
|
+
if (typeof value !== "string") {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const preset = value.trim().toLowerCase();
|
|
75
|
+
return isValidPreset(preset) ? preset : null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
79
|
+
// Runtime state
|
|
80
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
81
|
+
|
|
82
|
+
export function createRuntimeState(
|
|
83
|
+
startupSettings: Record<string, unknown>,
|
|
84
|
+
): RuntimeState {
|
|
85
|
+
const resolvedShortcuts = resolveShortcutConfig(startupSettings);
|
|
86
|
+
const bashModeSettings = parseBashModeSettings(
|
|
87
|
+
startupSettings,
|
|
88
|
+
resolvedShortcuts,
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const rt = {
|
|
92
|
+
enabled: true,
|
|
93
|
+
sessionStartTime: Date.now(),
|
|
94
|
+
sessionGeneration: 0,
|
|
95
|
+
currentCtx: null,
|
|
96
|
+
footerDataRef: null,
|
|
97
|
+
getThinkingLevelFn: null,
|
|
98
|
+
currentThinkingLevel: null,
|
|
99
|
+
liveAssistantUsage: null,
|
|
100
|
+
isStreaming: false,
|
|
101
|
+
tuiRef: null,
|
|
102
|
+
restoreFooterStatusRepaintHook: null,
|
|
103
|
+
stashShortcutInputUnsubscribe: null,
|
|
104
|
+
dismissWelcomeOverlay: null,
|
|
105
|
+
welcomeHeaderActive: false,
|
|
106
|
+
welcomeOverlayShouldDismiss: false,
|
|
107
|
+
lastUserPrompt: "",
|
|
108
|
+
showLastPrompt: true,
|
|
109
|
+
stashedEditorText: null,
|
|
110
|
+
stashedPromptHistory: readPersistedStashHistory(),
|
|
111
|
+
currentEditor: null,
|
|
112
|
+
bashModeActive: false,
|
|
113
|
+
bashTranscript: new BashTranscriptStore(bashModeSettings),
|
|
114
|
+
bashCompletionEngine: new BashCompletionEngine(),
|
|
115
|
+
shellSession: null,
|
|
116
|
+
queueStore: new PowerlineQueueStore(),
|
|
117
|
+
powerlineCompacting: false,
|
|
118
|
+
deliverAfterRetrySettles: false,
|
|
119
|
+
queueDeliveryTimer: null,
|
|
120
|
+
|
|
121
|
+
lastLayoutWidth: 0,
|
|
122
|
+
lastLayoutResult: null,
|
|
123
|
+
lastLayoutTimestamp: 0,
|
|
124
|
+
layoutDirty: true,
|
|
125
|
+
forceNextLayoutRecompute: false,
|
|
126
|
+
lastEditorInputAt: 0,
|
|
127
|
+
|
|
128
|
+
sessionBranchCache: new SessionBranchCache(),
|
|
129
|
+
tokenStatsCache: new SessionTokenStatsCache(),
|
|
130
|
+
coreContextUsageCache: new CoreContextUsageCache(),
|
|
131
|
+
|
|
132
|
+
resolvedShortcuts,
|
|
133
|
+
bashModeSettings,
|
|
134
|
+
} as RuntimeState;
|
|
135
|
+
|
|
136
|
+
rt.welcomeDismissScheduler = createWelcomeDismissScheduler({
|
|
137
|
+
dismiss: (ctx: unknown) => dismissWelcome(rt, ctx),
|
|
138
|
+
getGeneration: () => rt.sessionGeneration,
|
|
139
|
+
isEnabled: () => rt.enabled,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
rt.statusRenderScheduler = createRenderScheduler(() => {
|
|
143
|
+
const msSinceInput = Date.now() - rt.lastEditorInputAt;
|
|
144
|
+
if (
|
|
145
|
+
rt.layoutDirty &&
|
|
146
|
+
!rt.forceNextLayoutRecompute &&
|
|
147
|
+
msSinceInput < EDITOR_STATUS_DEFER_MS
|
|
148
|
+
) {
|
|
149
|
+
rt.statusRenderScheduler.schedule(
|
|
150
|
+
Math.max(0, EDITOR_STATUS_DEFER_MS - msSinceInput),
|
|
151
|
+
);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
rt.tuiRef?.requestRender();
|
|
156
|
+
}, STATUS_RENDER_DEBOUNCE_MS);
|
|
157
|
+
|
|
158
|
+
return rt;
|
|
159
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
3
|
+
|
|
4
|
+
import type { SegmentContext } from "../config/types.ts";
|
|
5
|
+
import { getPreset } from "../config/presets.ts";
|
|
6
|
+
import {
|
|
7
|
+
collectHiddenExtensionStatusKeys,
|
|
8
|
+
getNotificationExtensionStatuses,
|
|
9
|
+
} from "../config/powerline-config.ts";
|
|
10
|
+
import { ansi, getFgAnsiCode } from "../theme/colors.ts";
|
|
11
|
+
|
|
12
|
+
import { computeResponsiveLayout } from "./layout.ts";
|
|
13
|
+
import { getQueueContext } from "./queue-context.ts";
|
|
14
|
+
import { buildSegmentContext } from "./segment-context.ts";
|
|
15
|
+
import {
|
|
16
|
+
EDITOR_STATUS_DEFER_MS,
|
|
17
|
+
LAYOUT_CACHE_TTL_MS,
|
|
18
|
+
STREAMING_LAYOUT_CACHE_TTL_MS,
|
|
19
|
+
} from "./constants.ts";
|
|
20
|
+
import { config } from "./state.ts";
|
|
21
|
+
import type { RuntimeState } from "./types.ts";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get cached responsive layout or compute fresh one.
|
|
25
|
+
* The segment context scans session state, so keep it stable across render bursts.
|
|
26
|
+
*/
|
|
27
|
+
export function getResponsiveLayout(
|
|
28
|
+
rt: RuntimeState,
|
|
29
|
+
width: number,
|
|
30
|
+
theme: Theme,
|
|
31
|
+
): { topContent: string; secondaryContent: string } {
|
|
32
|
+
const now = Date.now();
|
|
33
|
+
const cacheTtl = rt.isStreaming
|
|
34
|
+
? STREAMING_LAYOUT_CACHE_TTL_MS
|
|
35
|
+
: LAYOUT_CACHE_TTL_MS;
|
|
36
|
+
|
|
37
|
+
if (rt.lastLayoutResult && rt.lastLayoutWidth === width) {
|
|
38
|
+
const msSinceInput = now - rt.lastEditorInputAt;
|
|
39
|
+
const typingRecently = msSinceInput < EDITOR_STATUS_DEFER_MS;
|
|
40
|
+
|
|
41
|
+
if (
|
|
42
|
+
!rt.forceNextLayoutRecompute &&
|
|
43
|
+
typingRecently &&
|
|
44
|
+
(rt.layoutDirty || now - rt.lastLayoutTimestamp >= cacheTtl)
|
|
45
|
+
) {
|
|
46
|
+
return rt.lastLayoutResult;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!rt.layoutDirty && now - rt.lastLayoutTimestamp < cacheTtl) {
|
|
50
|
+
return rt.lastLayoutResult;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const presetDef = getPreset(config.preset);
|
|
55
|
+
let segmentCtx: SegmentContext;
|
|
56
|
+
try {
|
|
57
|
+
segmentCtx = buildSegmentContext(rt, rt.currentCtx, theme);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
const isStale = error instanceof Error && (error.message.includes("This extension instance is stale") || error.message.includes("This extension ctx is stale"));
|
|
60
|
+
if (!isStale) throw error;
|
|
61
|
+
rt.currentCtx = null;
|
|
62
|
+
rt.lastLayoutWidth = width;
|
|
63
|
+
rt.lastLayoutResult = { topContent: "", secondaryContent: "" };
|
|
64
|
+
rt.lastLayoutTimestamp = now;
|
|
65
|
+
rt.layoutDirty = false;
|
|
66
|
+
rt.forceNextLayoutRecompute = false;
|
|
67
|
+
return rt.lastLayoutResult;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
rt.lastLayoutWidth = width;
|
|
71
|
+
rt.lastLayoutResult = computeResponsiveLayout(segmentCtx, presetDef, width);
|
|
72
|
+
rt.lastLayoutTimestamp = now;
|
|
73
|
+
rt.layoutDirty = false;
|
|
74
|
+
rt.forceNextLayoutRecompute = false;
|
|
75
|
+
|
|
76
|
+
return rt.lastLayoutResult;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function renderPowerlineStatusLines(
|
|
80
|
+
rt: RuntimeState,
|
|
81
|
+
width: number,
|
|
82
|
+
): string[] {
|
|
83
|
+
if (!rt.currentCtx || !rt.footerDataRef) return [];
|
|
84
|
+
|
|
85
|
+
const statuses = rt.footerDataRef.getExtensionStatuses();
|
|
86
|
+
if (!statuses || statuses.size === 0) return [];
|
|
87
|
+
const hiddenExtensionStatusKeys = collectHiddenExtensionStatusKeys(
|
|
88
|
+
config.customItems,
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const notifications: string[] = [];
|
|
92
|
+
for (const value of getNotificationExtensionStatuses(
|
|
93
|
+
statuses,
|
|
94
|
+
hiddenExtensionStatusKeys,
|
|
95
|
+
)) {
|
|
96
|
+
const lineContent = ` ${value}`;
|
|
97
|
+
if (visibleWidth(lineContent) <= width) {
|
|
98
|
+
notifications.push(lineContent);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return notifications;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function renderPowerlinePrimaryLines(
|
|
106
|
+
rt: RuntimeState,
|
|
107
|
+
width: number,
|
|
108
|
+
theme: Theme,
|
|
109
|
+
): string[] {
|
|
110
|
+
if (!rt.currentCtx) return [];
|
|
111
|
+
|
|
112
|
+
const layout = getResponsiveLayout(rt, width, theme);
|
|
113
|
+
return layout.topContent ? [layout.topContent] : [];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function renderPowerlineSecondaryLines(
|
|
117
|
+
rt: RuntimeState,
|
|
118
|
+
width: number,
|
|
119
|
+
theme: Theme,
|
|
120
|
+
): string[] {
|
|
121
|
+
if (!rt.currentCtx) return [];
|
|
122
|
+
|
|
123
|
+
const layout = getResponsiveLayout(rt, width, theme);
|
|
124
|
+
return layout.secondaryContent ? [layout.secondaryContent] : [];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function renderPowerlineQueuePreviewLines(
|
|
128
|
+
rt: RuntimeState,
|
|
129
|
+
width: number,
|
|
130
|
+
theme: Theme,
|
|
131
|
+
): string[] {
|
|
132
|
+
if (!rt.currentCtx) return [];
|
|
133
|
+
const summary = rt.queueStore.summarize(
|
|
134
|
+
getQueueContext(rt.currentCtx),
|
|
135
|
+
rt.powerlineCompacting,
|
|
136
|
+
);
|
|
137
|
+
if (!summary.leadingText) return [];
|
|
138
|
+
|
|
139
|
+
const prefix =
|
|
140
|
+
summary.leadingStatus === "blocked" || summary.leadingStatus === "failed"
|
|
141
|
+
? "blocked: "
|
|
142
|
+
: summary.leadingStatus === "delivering"
|
|
143
|
+
? "sending: "
|
|
144
|
+
: summary.leadingIntent === "idea"
|
|
145
|
+
? "idea: "
|
|
146
|
+
: "queued: ";
|
|
147
|
+
const text = `${prefix}${summary.leadingText.replace(/\s+/g, " ").trim()}`;
|
|
148
|
+
const color =
|
|
149
|
+
summary.leadingStatus === "blocked" || summary.leadingStatus === "failed"
|
|
150
|
+
? "warning"
|
|
151
|
+
: "dim";
|
|
152
|
+
return [
|
|
153
|
+
` ${theme.fg(color, truncateToWidth(text, Math.max(1, width - 1), "…"))}`,
|
|
154
|
+
];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function renderBashTranscriptLines(
|
|
158
|
+
rt: RuntimeState,
|
|
159
|
+
width: number,
|
|
160
|
+
theme: Theme,
|
|
161
|
+
): string[] {
|
|
162
|
+
if (!rt.bashModeActive) return [];
|
|
163
|
+
|
|
164
|
+
const snapshot = rt.bashTranscript.getSnapshot();
|
|
165
|
+
if (snapshot.commands.length === 0) return [];
|
|
166
|
+
|
|
167
|
+
const lines: string[] = [];
|
|
168
|
+
if (snapshot.truncatedCommands > 0) {
|
|
169
|
+
lines.push(
|
|
170
|
+
` ${theme.fg("dim", `… ${snapshot.truncatedCommands} earlier command${snapshot.truncatedCommands === 1 ? "" : "s"} truncated`)}`,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const recentCommands = snapshot.commands.slice(-4);
|
|
175
|
+
for (const command of recentCommands) {
|
|
176
|
+
const promptGlyph =
|
|
177
|
+
(rt.shellSession?.state.shellName ?? "shell") === "fish" ? ">" : "$";
|
|
178
|
+
const status =
|
|
179
|
+
command.exitCode === null
|
|
180
|
+
? theme.fg("accent", "running")
|
|
181
|
+
: command.exitCode === 0
|
|
182
|
+
? theme.fg("success", "ok")
|
|
183
|
+
: theme.fg("error", `exit ${command.exitCode}`);
|
|
184
|
+
const commandLine = truncateToWidth(
|
|
185
|
+
command.command.replace(/\s+/g, " ").trim(),
|
|
186
|
+
Math.max(8, width - 8),
|
|
187
|
+
"…",
|
|
188
|
+
);
|
|
189
|
+
lines.push(
|
|
190
|
+
` ${theme.fg("accent", promptGlyph)} ${commandLine} ${theme.fg("dim", "(")}${status}${theme.fg("dim", ")")}`,
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
const outputTail = command.output.slice(-6);
|
|
194
|
+
for (const outputLine of outputTail) {
|
|
195
|
+
lines.push(
|
|
196
|
+
` ${truncateToWidth(outputLine, Math.max(1, width - 3), "…")}`,
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return lines.slice(-16);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function renderLastPromptLines(
|
|
205
|
+
rt: RuntimeState,
|
|
206
|
+
width: number,
|
|
207
|
+
): string[] {
|
|
208
|
+
if (rt.bashModeActive || !rt.showLastPrompt || !rt.lastUserPrompt) return [];
|
|
209
|
+
|
|
210
|
+
const prefix = ` ${getFgAnsiCode("sep")}↳${ansi.reset} `;
|
|
211
|
+
const availableWidth = width - visibleWidth(prefix);
|
|
212
|
+
if (availableWidth < 10) return [];
|
|
213
|
+
|
|
214
|
+
let promptText = rt.lastUserPrompt.replace(/\s+/g, " ").trim();
|
|
215
|
+
if (!promptText) return [];
|
|
216
|
+
|
|
217
|
+
promptText = truncateToWidth(promptText, availableWidth, "…");
|
|
218
|
+
|
|
219
|
+
const styledPrompt = `${getFgAnsiCode("sep")}${promptText}${ansi.reset}`;
|
|
220
|
+
const line = `${prefix}${styledPrompt}`;
|
|
221
|
+
return [truncateToWidth(line, width, "…")];
|
|
222
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { ReadonlyFooterDataProvider } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
import type { BashModeSettings } from "../../bash-mode/types.ts";
|
|
4
|
+
import type { BashTranscriptStore } from "../../bash-mode/transcript.ts";
|
|
5
|
+
import type { BashCompletionEngine } from "../../bash-mode/completion.ts";
|
|
6
|
+
import type { ManagedShellSession } from "../../bash-mode/shell-session.ts";
|
|
7
|
+
import type { PowerlineQueueStore } from "../../queue/store.ts";
|
|
8
|
+
import type {
|
|
9
|
+
SessionAssistantUsage,
|
|
10
|
+
SessionBranchCache,
|
|
11
|
+
SessionTokenStatsCache,
|
|
12
|
+
} from "../usage/ledger.ts";
|
|
13
|
+
import type { CoreContextUsageCache } from "../usage/context.ts";
|
|
14
|
+
import type { WelcomeDismissScheduler } from "../welcome/auto-dismiss.ts";
|
|
15
|
+
import type { RenderScheduler } from "../render/timer.ts";
|
|
16
|
+
|
|
17
|
+
export type ShortcutBinding = string | null;
|
|
18
|
+
|
|
19
|
+
export interface PowerlineShortcuts {
|
|
20
|
+
stashHistory: ShortcutBinding;
|
|
21
|
+
copyEditor: ShortcutBinding;
|
|
22
|
+
cutEditor: ShortcutBinding;
|
|
23
|
+
ideaCapture: ShortcutBinding;
|
|
24
|
+
queueOpen: ShortcutBinding;
|
|
25
|
+
editorStart: ShortcutBinding;
|
|
26
|
+
editorEnd: ShortcutBinding;
|
|
27
|
+
/** Open the powerline main menu (navigate segments / configure / info). */
|
|
28
|
+
menu: ShortcutBinding;
|
|
29
|
+
/** Open the powerline info view (full open-ports list, TPS detail). */
|
|
30
|
+
info: ShortcutBinding;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type PowerlineShortcutKey = keyof PowerlineShortcuts;
|
|
34
|
+
export type PowerlineShortcutAction =
|
|
35
|
+
| { kind: "stashHistory" }
|
|
36
|
+
| { kind: "copyEditor" }
|
|
37
|
+
| { kind: "cutEditor" }
|
|
38
|
+
| { kind: "ideaCapture" }
|
|
39
|
+
| { kind: "queueOpen" }
|
|
40
|
+
| { kind: "bashMode" }
|
|
41
|
+
| { kind: "powerlineMenu" }
|
|
42
|
+
| { kind: "powerlineInfo" };
|
|
43
|
+
|
|
44
|
+
export interface RuntimeState {
|
|
45
|
+
enabled: boolean;
|
|
46
|
+
sessionStartTime: number;
|
|
47
|
+
sessionGeneration: number;
|
|
48
|
+
currentCtx: any;
|
|
49
|
+
footerDataRef: ReadonlyFooterDataProvider | null;
|
|
50
|
+
getThinkingLevelFn: (() => string) | null;
|
|
51
|
+
currentThinkingLevel: string | null;
|
|
52
|
+
liveAssistantUsage: SessionAssistantUsage | null;
|
|
53
|
+
isStreaming: boolean;
|
|
54
|
+
tuiRef: any;
|
|
55
|
+
restoreFooterStatusRepaintHook: (() => void) | null;
|
|
56
|
+
stashShortcutInputUnsubscribe: (() => void) | null;
|
|
57
|
+
dismissWelcomeOverlay: (() => void) | null;
|
|
58
|
+
welcomeHeaderActive: boolean;
|
|
59
|
+
welcomeOverlayShouldDismiss: boolean;
|
|
60
|
+
lastUserPrompt: string;
|
|
61
|
+
showLastPrompt: boolean;
|
|
62
|
+
stashedEditorText: string | null;
|
|
63
|
+
stashedPromptHistory: string[];
|
|
64
|
+
currentEditor: any;
|
|
65
|
+
bashModeActive: boolean;
|
|
66
|
+
bashTranscript: BashTranscriptStore;
|
|
67
|
+
bashCompletionEngine: BashCompletionEngine;
|
|
68
|
+
shellSession: ManagedShellSession | null;
|
|
69
|
+
queueStore: PowerlineQueueStore;
|
|
70
|
+
powerlineCompacting: boolean;
|
|
71
|
+
deliverAfterRetrySettles: boolean;
|
|
72
|
+
queueDeliveryTimer: ReturnType<typeof setTimeout> | null;
|
|
73
|
+
|
|
74
|
+
// Cache for the top and secondary powerline widgets.
|
|
75
|
+
lastLayoutWidth: number;
|
|
76
|
+
lastLayoutResult: {
|
|
77
|
+
topContent: string;
|
|
78
|
+
secondaryContent: string;
|
|
79
|
+
} | null;
|
|
80
|
+
lastLayoutTimestamp: number;
|
|
81
|
+
layoutDirty: boolean;
|
|
82
|
+
forceNextLayoutRecompute: boolean;
|
|
83
|
+
lastEditorInputAt: number;
|
|
84
|
+
|
|
85
|
+
// Cache for token counting: avoid re-scanning the full session event list
|
|
86
|
+
// on every render (250ms-1s cadence). Revalidates the trailing event's
|
|
87
|
+
// stats signature so in-place streaming updates are not served stale.
|
|
88
|
+
sessionBranchCache: SessionBranchCache;
|
|
89
|
+
tokenStatsCache: SessionTokenStatsCache;
|
|
90
|
+
coreContextUsageCache: CoreContextUsageCache;
|
|
91
|
+
|
|
92
|
+
resolvedShortcuts: PowerlineShortcuts;
|
|
93
|
+
bashModeSettings: BashModeSettings;
|
|
94
|
+
|
|
95
|
+
welcomeDismissScheduler: WelcomeDismissScheduler<any>;
|
|
96
|
+
statusRenderScheduler: RenderScheduler;
|
|
97
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
generateVibesBatch,
|
|
5
|
+
getVibeFileCount,
|
|
6
|
+
getVibeMode,
|
|
7
|
+
getVibeModel,
|
|
8
|
+
getVibeTheme,
|
|
9
|
+
hasVibeFile,
|
|
10
|
+
parseVibeGenerateArgs,
|
|
11
|
+
setVibeMode,
|
|
12
|
+
setVibeModel,
|
|
13
|
+
setVibeTheme,
|
|
14
|
+
} from "../working-vibes/index.ts";
|
|
15
|
+
|
|
16
|
+
export async function runVibeCommand(args: string, ctx: any): Promise<void> {
|
|
17
|
+
const parts = args?.trim().split(/\s+/) || [];
|
|
18
|
+
const subcommand = parts[0]?.toLowerCase();
|
|
19
|
+
|
|
20
|
+
// No args: show current status
|
|
21
|
+
if (!args || !args.trim()) {
|
|
22
|
+
const theme = getVibeTheme();
|
|
23
|
+
const mode = getVibeMode();
|
|
24
|
+
const model = getVibeModel();
|
|
25
|
+
let status = `Vibe: ${theme || "off"} | Mode: ${mode} | Model: ${model}`;
|
|
26
|
+
if (theme && mode === "file") {
|
|
27
|
+
const count = getVibeFileCount(theme);
|
|
28
|
+
status +=
|
|
29
|
+
count > 0 ? ` | File: ${count} vibes` : " | File: not found";
|
|
30
|
+
}
|
|
31
|
+
ctx.ui.notify(status, "info");
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// /vibe model [spec] - show or set model
|
|
36
|
+
if (subcommand === "model") {
|
|
37
|
+
const modelSpec = parts.slice(1).join(" ");
|
|
38
|
+
if (!modelSpec) {
|
|
39
|
+
ctx.ui.notify(`Current vibe model: ${getVibeModel()}`, "info");
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
// Validate format (provider/modelId)
|
|
43
|
+
if (!modelSpec.includes("/")) {
|
|
44
|
+
ctx.ui.notify(
|
|
45
|
+
"Invalid model format. Use: provider/modelId (e.g., openai-codex/gpt-5.4-mini)",
|
|
46
|
+
"error",
|
|
47
|
+
);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const persisted = setVibeModel(modelSpec);
|
|
51
|
+
if (persisted) {
|
|
52
|
+
ctx.ui.notify(`Vibe model set to: ${modelSpec}`, "info");
|
|
53
|
+
} else {
|
|
54
|
+
ctx.ui.notify(
|
|
55
|
+
`Vibe model set to: ${modelSpec} (not persisted; check settings.json)`,
|
|
56
|
+
"warning",
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// /vibe mode [generate|file] - show or set mode
|
|
63
|
+
if (subcommand === "mode") {
|
|
64
|
+
const newMode = parts[1]?.toLowerCase();
|
|
65
|
+
if (!newMode) {
|
|
66
|
+
ctx.ui.notify(`Current vibe mode: ${getVibeMode()}`, "info");
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (newMode !== "generate" && newMode !== "file") {
|
|
70
|
+
ctx.ui.notify("Invalid mode. Use: generate or file", "error");
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
// Check if file exists when switching to file mode
|
|
74
|
+
const theme = getVibeTheme();
|
|
75
|
+
if (newMode === "file" && theme && !hasVibeFile(theme)) {
|
|
76
|
+
ctx.ui.notify(
|
|
77
|
+
`No vibe file for "${theme}". Run /vibe generate ${theme} first`,
|
|
78
|
+
"error",
|
|
79
|
+
);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const persisted = setVibeMode(newMode);
|
|
83
|
+
if (persisted) {
|
|
84
|
+
ctx.ui.notify(`Vibe mode set to: ${newMode}`, "info");
|
|
85
|
+
} else {
|
|
86
|
+
ctx.ui.notify(
|
|
87
|
+
`Vibe mode set to: ${newMode} (not persisted; check settings.json)`,
|
|
88
|
+
"warning",
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// /vibe generate <theme> [count] - generate vibes and save to file
|
|
95
|
+
if (subcommand === "generate") {
|
|
96
|
+
const parsed = parseVibeGenerateArgs(parts.slice(1));
|
|
97
|
+
if (!parsed) {
|
|
98
|
+
ctx.ui.notify("Usage: /vibe generate <theme> [count]", "error");
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const { theme, count } = parsed;
|
|
103
|
+
ctx.ui.notify(`Generating ${count} vibes for "${theme}"...`, "info");
|
|
104
|
+
|
|
105
|
+
const result = await generateVibesBatch(theme, count);
|
|
106
|
+
|
|
107
|
+
if (result.success) {
|
|
108
|
+
ctx.ui.notify(
|
|
109
|
+
`Generated ${result.count} vibes for "${theme}" → ${result.filePath}`,
|
|
110
|
+
"info",
|
|
111
|
+
);
|
|
112
|
+
} else {
|
|
113
|
+
ctx.ui.notify(`Failed to generate vibes: ${result.error}`, "error");
|
|
114
|
+
}
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// /vibe off - disable
|
|
119
|
+
if (subcommand === "off") {
|
|
120
|
+
const persisted = setVibeTheme(null);
|
|
121
|
+
if (persisted) {
|
|
122
|
+
ctx.ui.notify("Vibe disabled", "info");
|
|
123
|
+
} else {
|
|
124
|
+
ctx.ui.notify(
|
|
125
|
+
"Vibe disabled (not persisted; check settings.json)",
|
|
126
|
+
"warning",
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// /vibe <theme> - set theme (preserve original casing)
|
|
133
|
+
const theme = args.trim();
|
|
134
|
+
const persisted = setVibeTheme(theme);
|
|
135
|
+
const mode = getVibeMode();
|
|
136
|
+
if (mode === "file" && !hasVibeFile(theme)) {
|
|
137
|
+
const suffix = persisted ? "" : " (not persisted; check settings.json)";
|
|
138
|
+
ctx.ui.notify(
|
|
139
|
+
`Vibe set to: ${theme} (file mode, but no file found - run /vibe generate ${theme})${suffix}`,
|
|
140
|
+
"warning",
|
|
141
|
+
);
|
|
142
|
+
} else if (persisted) {
|
|
143
|
+
ctx.ui.notify(`Vibe set to: ${theme}`, "info");
|
|
144
|
+
} else {
|
|
145
|
+
ctx.ui.notify(
|
|
146
|
+
`Vibe set to: ${theme} (not persisted; check settings.json)`,
|
|
147
|
+
"warning",
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function registerVibeCommand(pi: ExtensionAPI): void {
|
|
153
|
+
pi.registerCommand("vibe", {
|
|
154
|
+
description:
|
|
155
|
+
"Set working message theme. Usage: /vibe [theme|off|mode|model|generate]",
|
|
156
|
+
handler: async (args, ctx) => {
|
|
157
|
+
runVibeCommand(args, ctx);
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { RuntimeState } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
export function dismissWelcome(rt: RuntimeState, ctx: any) {
|
|
4
|
+
rt.welcomeDismissScheduler.cancel();
|
|
5
|
+
|
|
6
|
+
if (rt.dismissWelcomeOverlay) {
|
|
7
|
+
rt.dismissWelcomeOverlay();
|
|
8
|
+
rt.dismissWelcomeOverlay = null;
|
|
9
|
+
} else {
|
|
10
|
+
// The startup overlay mounts after a delay; dismiss it immediately if it appears later.
|
|
11
|
+
rt.welcomeOverlayShouldDismiss = true;
|
|
12
|
+
}
|
|
13
|
+
if (rt.welcomeHeaderActive) {
|
|
14
|
+
rt.welcomeHeaderActive = false;
|
|
15
|
+
ctx.ui.setHeader(undefined);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function scheduleDismissWelcome(rt: RuntimeState, ctx: any) {
|
|
20
|
+
if (
|
|
21
|
+
!rt.dismissWelcomeOverlay &&
|
|
22
|
+
rt.welcomeOverlayShouldDismiss &&
|
|
23
|
+
!rt.welcomeHeaderActive
|
|
24
|
+
)
|
|
25
|
+
return;
|
|
26
|
+
rt.welcomeDismissScheduler.schedule(ctx);
|
|
27
|
+
}
|