@groeponline/pi-wishcraft 1.4.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +28 -0
- package/CHANGELOG.md +25 -0
- package/README.md +22 -18
- package/ROADMAP.md +3 -3
- package/STRATEGY.md +54 -0
- package/bash-mode/editor-input.ts +3 -1
- package/bash-mode/editor.ts +44 -7
- package/bash-mode/forward.ts +64 -0
- package/bash-mode/pty-session.ts +356 -0
- package/bash-mode/ptyshell-managed.ts +145 -0
- package/bash-mode/session-factory.ts +64 -0
- package/bash-mode/transcript-v2.ts +77 -0
- package/bash-mode/transcript.ts +18 -0
- package/bash-mode/types.ts +27 -0
- package/docs/bash-mode.md +4 -0
- package/docs/configuration.md +45 -1
- package/docs/index.md +4 -1
- package/package.json +1 -1
- package/src/config/effective.ts +122 -0
- package/src/config/settings-registry.ts +2 -0
- package/src/config/structural-presets.ts +1 -1
- package/src/extension/commands/bash-mode-actions.ts +11 -10
- package/src/extension/commands/commands.ts +8 -0
- package/src/extension/core/state.ts +0 -1
- package/src/extension/core/types.ts +2 -2
- package/src/extension/session/session-lifecycle.ts +21 -0
- package/src/extension/skills/skill-templates.ts +12 -3
- package/src/extension/ui/custom-editor.ts +4 -0
- package/src/extension/ui/status-line-renderers.ts +6 -7
- package/src/extension/welcome/welcome-integration.ts +23 -1
- package/src/motion/frames.ts +19 -9
- package/src/render/layout.ts +95 -0
- package/src/render/motion-rail.ts +79 -0
- package/src/render/paint.ts +40 -0
- package/src/render/v2-adapter.ts +68 -0
- package/src/render/v2-entry.ts +132 -0
- package/src/segments/pipeline.ts +164 -0
- package/src/segments/types.ts +36 -0
- package/src/segments/usage.ts +42 -3
- package/src/studio/actions.ts +105 -0
- package/src/studio/advice-pane.ts +73 -0
- package/src/studio/advise/context.ts +89 -0
- package/src/studio/advise/engine.ts +117 -0
- package/src/studio/advise/prompts.ts +68 -0
- package/src/studio/component.ts +105 -0
- package/src/studio/deepwiki/cache.ts +74 -0
- package/src/studio/deepwiki/client.ts +73 -0
- package/src/studio/deepwiki/extract.ts +66 -0
- package/src/studio/inspect.ts +38 -0
- package/src/studio/list.ts +48 -0
- package/src/studio/open.ts +44 -0
- package/src/studio/state.ts +100 -0
- package/src/studio/types.ts +30 -0
- package/src/welcome/banner.ts +6 -0
- package/src/welcome/index.ts +7 -0
- package/src/welcome/overlay.ts +6 -0
- package/src/welcome/renderer.ts +6 -39
- package/src/welcome/types.ts +6 -0
- package/src/welcome/welcome-art.ts +148 -0
- package/src/working-vibes/frames.ts +25 -0
- package/src/working-vibes/index.ts +2 -0
- package/src/working-vibes/storage.ts +11 -0
- package/bash-mode/shell-session.ts +0 -294
- package/src/signal/render.ts +0 -221
package/bash-mode/types.ts
CHANGED
|
@@ -64,6 +64,24 @@ export interface ShellSessionState {
|
|
|
64
64
|
lastExitCode: number | null;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Common surface for the PTY-backed managed session; kept as a seam so the
|
|
69
|
+
* editor/actions wiring does not depend on PtyManagedShellSession internals.
|
|
70
|
+
* writeStdin is optional-callable via the seam (v2 exposes it).
|
|
71
|
+
*/
|
|
72
|
+
export interface ManagedShellSessionLike {
|
|
73
|
+
readonly state: ShellSessionState;
|
|
74
|
+
ensureReady(): Promise<void>;
|
|
75
|
+
runCommand(command: string): Promise<void>;
|
|
76
|
+
interrupt(): void;
|
|
77
|
+
dispose(): void;
|
|
78
|
+
writeStdin?(data: string): void;
|
|
79
|
+
/** Whether the session supports editor forward-mode (v2 PTY only). */
|
|
80
|
+
supportsForwardMode(): boolean;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export { createForwardState, handleForwardInput, type ForwardState, type ForwardDecision, type PtyAction } from "./forward.ts";
|
|
84
|
+
|
|
67
85
|
export interface BashModeEditorOptions {
|
|
68
86
|
keybindings: KeybindingsManager;
|
|
69
87
|
isBashModeActive: () => boolean;
|
|
@@ -73,6 +91,15 @@ export interface BashModeEditorOptions {
|
|
|
73
91
|
onEditorSubmit?: () => void;
|
|
74
92
|
editorBoundaryShortcuts?: EditorBoundaryShortcuts;
|
|
75
93
|
onInterrupt: () => void;
|
|
94
|
+
/** Forward raw input to a running shell (v2 PTY forward-mode, U4). */
|
|
95
|
+
onForwardInput?: (data: string) => void;
|
|
96
|
+
/**
|
|
97
|
+
* Enable forward-mode: while a command runs, printable input routes to
|
|
98
|
+
* the PTY stdin instead of the editor (AE1). v1 run-blocked behavior is
|
|
99
|
+
* the default until a session opts in. Evaluated per keystroke so the
|
|
100
|
+
* session kind (v1 vs v2) decides at input time.
|
|
101
|
+
*/
|
|
102
|
+
forwardWhileRunning?: () => boolean;
|
|
76
103
|
onNotify: (message: string, level?: "info" | "warning" | "error") => void;
|
|
77
104
|
getHistoryEntries: (prefix: string) => string[];
|
|
78
105
|
resolveGhostSuggestion: (
|
package/docs/bash-mode.md
CHANGED
|
@@ -20,6 +20,10 @@ While bash mode is active:
|
|
|
20
20
|
|
|
21
21
|
The managed shell is persistent for the current pi session. Command output appears in a transcript below the editor, and shell cwd changes are reflected in the footer path and `shell_mode` segment.
|
|
22
22
|
|
|
23
|
+
## Execution (v2)
|
|
24
|
+
|
|
25
|
+
Commands run under a real PTY via `script(1)` (no native dependency), so programs that read stdin work: printable input typed while a command runs is forwarded to the process, and `ctrl+c` interrupts it. SGR color survives into the transcript when the terminal supports it; `NO_COLOR` renders plain text. When `script(1)` is missing, each command degrades to plain pipe execution with a one-time warning (no color, no interactive stdin).
|
|
26
|
+
|
|
23
27
|
## Shell ghost suggestions
|
|
24
28
|
|
|
25
29
|
Bash mode is ghost-first. Successful per-project shell history is the primary source, while deterministic path and git continuations can still extend an existing command. Shell-native completion probes are disabled so `!command` predictions never spawn interactive shell completion subprocesses.
|
package/docs/configuration.md
CHANGED
|
@@ -190,6 +190,37 @@ Make `open_ports` probe a named SSH host instead of the laptop:
|
|
|
190
190
|
|
|
191
191
|
See [Segments & theming](./segments.md) for the probe's best-effort behavior and requirements.
|
|
192
192
|
|
|
193
|
+
## Bash Mode performance and workflow
|
|
194
|
+
|
|
195
|
+
Bash Mode keeps a project-scoped command history, supports Bash/Zsh/Fish history recall, Git-aware completion, path completion, file drops, ghost suggestions, and safe `!`/`!!` commands. Ghost suggestions are coalesced during rapid typing and cancelled when the editor context changes, so completion never blocks command entry.
|
|
196
|
+
|
|
197
|
+
Use `ctrl+shift+b` to toggle Bash Mode. `Escape` exits the mode, `Tab` accepts one completion token, `Right Arrow` accepts the full ghost suggestion, and the configured editor-boundary shortcuts jump to the start/end of the input. Commands are not submitted while another shell command is running.
|
|
198
|
+
|
|
199
|
+
## Working indicator
|
|
200
|
+
|
|
201
|
+
The agent's working indicator can use a compact animated frame style. Each style has a distinct visual grammar: `dots` is a quiet three-dot cadence, `pulse` is a centered heartbeat, `bar` is a travelling progress sweep, and `ascii` is a terminal-safe bracketed spinner. Accessibility settings can collapse all four to a static frame.
|
|
202
|
+
|
|
203
|
+
The indicator is intentionally separate from the structural appearance motion: it describes immediate agent activity, while appearance motion describes Signal/deck transitions and ambient state. Choose one of the following depending on the desired feel:
|
|
204
|
+
|
|
205
|
+
| Style | Character | Best for |
|
|
206
|
+
|---|---|---|
|
|
207
|
+
| `dots` | gentle sequential dots | unobtrusive everyday use |
|
|
208
|
+
| `pulse` | expanding/contracting center | clear activity at a glance |
|
|
209
|
+
| `bar` | travelling block across a track | high-information dashboards |
|
|
210
|
+
| `ascii` | portable `[=]`, `[-]`, `[.]` frames | SSH, basic terminals, and logs |
|
|
211
|
+
|
|
212
|
+
Configure it under `wishcraft.workingIndicatorStyle`; supported values are `dots` (default), `pulse`, `bar`, and `ascii`. Motion accessibility settings still apply: reduced/off motion uses a static frame.
|
|
213
|
+
|
|
214
|
+
```json
|
|
215
|
+
{
|
|
216
|
+
"wishcraft": {
|
|
217
|
+
"workingIndicatorStyle": "pulse"
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
The Skills route in the Deck and `/skills` manager share one catalog. Use `/skills doctor` to inspect frontmatter, descriptions, duplicate names, warnings, and usage; `/skills new [template]` creates a safe starter skill. Skill deletion is restricted to canonical skill roots.
|
|
223
|
+
|
|
193
224
|
## Appearance
|
|
194
225
|
|
|
195
226
|
`powerline.appearance` is independent of the information layout (`powerline.preset`). The structural base paints Signal colors and motion. Layout presets (`default`, `minimal`, `compact`, `full`, `nerd`, `ascii`, `chef`) keep their segment lists until you change `preset`.
|
|
@@ -205,7 +236,20 @@ See [Segments & theming](./segments.md) for the probe's best-effort behavior and
|
|
|
205
236
|
}
|
|
206
237
|
```
|
|
207
238
|
|
|
208
|
-
Bases: `lanternwake`, `threadbound`, `scryglass`, `runebloom`, `moonwell`, `hexforge`, `vellum`, `wisp`, `starweave`, `crucible`. Apply from Deck → Appearance → Enter, `/wishcraft settings`, or `/signal hexforge` (a structural layout name also writes `appearance.base`). Optional mix keys: `palette`, `signalLayout`, `chrome`, `glyphs`, `deck`, `welcome`, `motion`.
|
|
239
|
+
Bases: `lanternwake`, `threadbound`, `scryglass`, `runebloom`, `moonwell`, `hexforge`, `vellum`, `wisp`, `starweave`, `crucible`. They are deliberately different rather than aliases: each has its own palette, chrome geometry, Signal lane layout, glyph vocabulary, welcome treatment, and signature motion event. Apply from Deck → Appearance → Enter, `/wishcraft settings`, or `/signal hexforge` (a structural layout name also writes `appearance.base`). Optional mix keys: `palette`, `signalLayout`, `chrome`, `glyphs`, `deck`, `welcome`, `motion`.
|
|
240
|
+
|
|
241
|
+
Quick personality guide:
|
|
242
|
+
|
|
243
|
+
- `lanternwake`: warm ember glow and relay motion; welcoming and expressive.
|
|
244
|
+
- `threadbound`: woven separators and linked transitions; structured and calm.
|
|
245
|
+
- `scryglass`: cool observatory palette; sparse, analytical readout.
|
|
246
|
+
- `runebloom`: ornamented glyphs and bloom transitions; mystical and decorative.
|
|
247
|
+
- `moonwell`: blue night palette with slow tidal motion; quiet long sessions.
|
|
248
|
+
- `hexforge`: hard-edged blocks and tool-centric motion; dense engineering mode.
|
|
249
|
+
- `vellum`: restrained paper-like contrast; documentation and review work.
|
|
250
|
+
- `wisp`: lightweight, airy signal; lowest visual weight while remaining animated.
|
|
251
|
+
- `starweave`: constellation accents and travelling signal; exploratory sessions.
|
|
252
|
+
- `crucible`: high-contrast heat and decisive transitions; incident/debug mode.
|
|
209
253
|
|
|
210
254
|
Until `appearance` is set, Signal keeps the layout preset colors. If `preset` itself is a structural name and `appearance` is empty, that name is treated as the base.
|
|
211
255
|
|
package/docs/index.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# pi-wishcraft documentation
|
|
2
2
|
|
|
3
|
-
The README is the
|
|
3
|
+
This package is a Pi.dev extension, not a standalone website. The README is the short installation and capability contract; the complete operator, configuration, and design references live here.
|
|
4
4
|
|
|
5
5
|
## Guides
|
|
6
6
|
|
|
@@ -14,6 +14,9 @@ The README is the public landing page (`banner.png` only). Everything below live
|
|
|
14
14
|
|
|
15
15
|
## Design System & vNext Specifications
|
|
16
16
|
|
|
17
|
+
These documents describe the terminal-native extension UI and are useful when contributing to or reviewing the package; they are not a web application specification.
|
|
18
|
+
|
|
19
|
+
|
|
17
20
|
- [vNext Stacked PR Release Plan](./design/vnext-release-plan.md) — The comprehensive implementation roadmap covering PR0 through PR8.
|
|
18
21
|
- [vNext Architecture & Overview](./design/vnext-overview.md) — Product vision, five pillars, and architectural boundaries.
|
|
19
22
|
- [Semantic Tokens & Theme Contract](./design/theme-contract.md) — `WishcraftTokens` schema and backward compatibility mappings.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@groeponline/pi-wishcraft",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "Operator cockpit for Pi: live powerline status, searchable skills, idea queue, sticky Bash, hooks, policy controls, and session UX.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Effective config resolver (U1, KTD4/5).
|
|
3
|
+
*
|
|
4
|
+
* Treats `SETTINGS_REGISTRY` as the single source of truth: every value the
|
|
5
|
+
* caller reads is a registered key, every unknown key is ignored, and every
|
|
6
|
+
* invalid value falls back to the registered default with a per-path error
|
|
7
|
+
* entry. The previous `parsePowerlineConfig` is a v1 consumer; new code reads
|
|
8
|
+
* through this resolver so powerline + skills + harness settings all live in
|
|
9
|
+
* one shape. Strangler: v1 still works until U11 cuts over.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
getSettingDefinition,
|
|
14
|
+
SETTING_DEFAULTS,
|
|
15
|
+
SETTINGS_REGISTRY,
|
|
16
|
+
type SettingDefinition,
|
|
17
|
+
type SettingValue,
|
|
18
|
+
} from "./settings-registry.ts";
|
|
19
|
+
|
|
20
|
+
export type SettingPath = keyof typeof SETTING_DEFAULTS;
|
|
21
|
+
|
|
22
|
+
export type SettingInput = Record<string, unknown>;
|
|
23
|
+
|
|
24
|
+
export interface SettingError {
|
|
25
|
+
path: string;
|
|
26
|
+
message: string;
|
|
27
|
+
rawValue: unknown;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface EffectiveConfig {
|
|
31
|
+
values: Record<SettingPath, SettingValue>;
|
|
32
|
+
errors: SettingError[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const SETTING_PATHS: readonly SettingPath[] = Object.keys(
|
|
36
|
+
SETTING_DEFAULTS,
|
|
37
|
+
) as SettingPath[];
|
|
38
|
+
|
|
39
|
+
function coerceToggle(value: unknown): boolean | null {
|
|
40
|
+
if (typeof value === "boolean") return value;
|
|
41
|
+
if (value === "on" || value === "true" || value === 1) return true;
|
|
42
|
+
if (value === "off" || value === "false" || value === 0) return false;
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function coerceNumber(value: unknown): number | null {
|
|
47
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
48
|
+
if (typeof value === "string") {
|
|
49
|
+
const trimmed = value.trim();
|
|
50
|
+
if (trimmed === "") return null;
|
|
51
|
+
// Full-string conversion: "1000ms" and friends must not silently
|
|
52
|
+
// coerce to a number; invalid values fall back to the default.
|
|
53
|
+
const parsed = Number(trimmed);
|
|
54
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function coerceText(value: unknown): string | null {
|
|
60
|
+
return typeof value === "string" ? value : null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function coerceSelect(value: unknown, choices: readonly string[]): string | null {
|
|
64
|
+
if (typeof value !== "string") return null;
|
|
65
|
+
return choices.includes(value) ? value : null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function coerce(definition: SettingDefinition, value: unknown): SettingValue | null {
|
|
69
|
+
if (definition.kind === "toggle") {
|
|
70
|
+
const coerced = coerceToggle(value);
|
|
71
|
+
return coerced;
|
|
72
|
+
}
|
|
73
|
+
if (definition.kind === "number") {
|
|
74
|
+
return coerceNumber(value);
|
|
75
|
+
}
|
|
76
|
+
if (definition.kind === "text") {
|
|
77
|
+
return coerceText(value);
|
|
78
|
+
}
|
|
79
|
+
return coerceSelect(value, definition.choices);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function defaultFor(definition: SettingDefinition): SettingValue | null {
|
|
83
|
+
if (definition.defaultValue !== undefined) return definition.defaultValue;
|
|
84
|
+
if (definition.kind === "toggle") return false;
|
|
85
|
+
if (definition.kind === "number") return 0;
|
|
86
|
+
if (definition.kind === "text") return "";
|
|
87
|
+
return "";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function resolveEffectiveConfig(input: SettingInput = {}): EffectiveConfig {
|
|
91
|
+
const values = {} as Record<SettingPath, SettingValue>;
|
|
92
|
+
const errors: SettingError[] = [];
|
|
93
|
+
|
|
94
|
+
for (const path of SETTING_PATHS) {
|
|
95
|
+
const definition = getSettingDefinition(path);
|
|
96
|
+
if (!definition) continue;
|
|
97
|
+
const raw = input[path];
|
|
98
|
+
if (raw === undefined) {
|
|
99
|
+
const fallback = defaultFor(definition);
|
|
100
|
+
if (fallback !== null) values[path] = fallback;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const coerced = coerce(definition, raw);
|
|
104
|
+
if (coerced === null) {
|
|
105
|
+
const fallback = defaultFor(definition);
|
|
106
|
+
if (fallback !== null) values[path] = fallback;
|
|
107
|
+
errors.push({
|
|
108
|
+
path,
|
|
109
|
+
message: `Invalid value for ${path}; using default.`,
|
|
110
|
+
rawValue: raw,
|
|
111
|
+
});
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
values[path] = coerced;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return { values, errors };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function knownSettings(): readonly SettingDefinition[] {
|
|
121
|
+
return SETTINGS_REGISTRY;
|
|
122
|
+
}
|
|
@@ -41,6 +41,7 @@ export const SETTING_DEFAULTS = {
|
|
|
41
41
|
"wishcraft.readHints": true,
|
|
42
42
|
"wishcraft.hooksEnabled": true,
|
|
43
43
|
"wishcraft.repairsEnabled": true,
|
|
44
|
+
"wishcraft.workingIndicatorStyle": "dots",
|
|
44
45
|
} as const satisfies Record<string, SettingValue>;
|
|
45
46
|
|
|
46
47
|
export const SETTING_GROUPS = [
|
|
@@ -80,6 +81,7 @@ export const SETTINGS_REGISTRY = [
|
|
|
80
81
|
{ id: "status.tps.label", path: "powerline.segmentLabels.tps", group: "status", label: "TPS label", kind: "text", hint: "empty = no label" },
|
|
81
82
|
{ id: "welcome.enabled", path: "powerline.welcome", group: "vibes", label: "Welcome overlay", kind: "toggle", defaultValue: SETTING_DEFAULTS["powerline.welcome"], hint: "on = overlay at startup, off = no welcome" },
|
|
82
83
|
{ id: "welcome.lanternMotion", path: "wishcraft.welcome.animateLantern", group: "vibes", label: "Animate wishcraft lantern", kind: "toggle", hint: "flicker on the lantern" },
|
|
84
|
+
{ id: "vibes.indicatorStyle", path: "wishcraft.workingIndicatorStyle", group: "vibes", label: "Working indicator", kind: "select", choices: ["dots", "pulse", "bar", "ascii"], defaultValue: SETTING_DEFAULTS["wishcraft.workingIndicatorStyle"], hint: "frame style used while the agent is working" },
|
|
83
85
|
{ id: "skills.inline", path: "wishcraft.inlineSkills", group: "skills", label: "Inline expand /command and $skill", kind: "toggle", hint: "needs a restart to take effect", restartRequired: true },
|
|
84
86
|
{ id: "skills.readHints", path: "wishcraft.readHints", group: "skills", label: "Read hints", kind: "toggle", defaultValue: SETTING_DEFAULTS["wishcraft.readHints"], hint: "off = no continuation hint after partial reads" },
|
|
85
87
|
{ id: "harness.hooks", path: "wishcraft.hooksEnabled", group: "harness", label: "Hooks enabled", kind: "toggle", defaultValue: SETTING_DEFAULTS["wishcraft.hooksEnabled"], hint: "gate for configured preToolUse / postToolUse / sessionStart hooks" },
|
|
@@ -279,7 +279,7 @@ export const STRUCTURAL_PRESETS: Record<StructuralPresetName, StructuralPresetDe
|
|
|
279
279
|
signal: {
|
|
280
280
|
layout: "arc",
|
|
281
281
|
separators: { left: "───", right: "───", subLeft: "◜", subRight: "◟" },
|
|
282
|
-
caps: { leftOpen: "
|
|
282
|
+
caps: { leftOpen: "", leftClose: "", rightOpen: "", rightClose: "" },
|
|
283
283
|
animation: "lunar-breathe",
|
|
284
284
|
},
|
|
285
285
|
motion: motion("lunar-breathe"),
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
readGlobalShellHistory,
|
|
5
5
|
readProjectHistory,
|
|
6
6
|
} from "../../../bash-mode/history.ts";
|
|
7
|
-
import {
|
|
7
|
+
import { createShellSession } from "../../../bash-mode/session-factory.ts";
|
|
8
8
|
import { requestStatusRender } from "../core/segment-context.ts";
|
|
9
9
|
import type { RuntimeState } from "../core/types.ts";
|
|
10
10
|
|
|
@@ -34,17 +34,18 @@ export const getShellHistoryEntries = (
|
|
|
34
34
|
|
|
35
35
|
export const ensureShellSession = async (
|
|
36
36
|
rt: RuntimeState,
|
|
37
|
-
): Promise<
|
|
37
|
+
): Promise<NonNullable<RuntimeState["shellSession"]>> => {
|
|
38
38
|
if (!rt.shellSession) {
|
|
39
|
-
rt.shellSession =
|
|
40
|
-
getShellPath(),
|
|
41
|
-
rt.currentCtx?.cwd ?? process.cwd(),
|
|
42
|
-
rt.bashTranscript,
|
|
43
|
-
|
|
44
|
-
(
|
|
39
|
+
rt.shellSession = createShellSession({
|
|
40
|
+
shellPath: getShellPath(),
|
|
41
|
+
cwd: rt.currentCtx?.cwd ?? process.cwd(),
|
|
42
|
+
transcript: rt.bashTranscript,
|
|
43
|
+
prefer: "auto",
|
|
44
|
+
onStateChange: () => requestStatusRender(rt),
|
|
45
|
+
onCommandSuccess: (command, cwd) =>
|
|
45
46
|
appendProjectHistory(rt.currentCtx?.cwd ?? process.cwd(), command, cwd),
|
|
46
|
-
rt.bashModeSettings.initScript,
|
|
47
|
-
);
|
|
47
|
+
initScript: rt.bashModeSettings.initScript,
|
|
48
|
+
});
|
|
48
49
|
}
|
|
49
50
|
await rt.shellSession.ensureReady();
|
|
50
51
|
return rt.shellSession;
|
|
@@ -23,6 +23,7 @@ import { showPowerlineClassicMenu } from "../ui/powerline-menu-view.ts";
|
|
|
23
23
|
import { syncAppearanceForLayoutPreset } from "../settings/appearance-write.ts";
|
|
24
24
|
import { openStashHistory } from "../shortcuts/shortcuts-router.ts";
|
|
25
25
|
import { ensureShellSession, setBashModeActive } from "./bash-mode-actions.ts";
|
|
26
|
+
import { openSkillStudio } from "../../studio/open.ts";
|
|
26
27
|
import { setupCustomEditor } from "../ui/custom-editor.ts";
|
|
27
28
|
import { getPromptHistoryState } from "../history/prompt-history.ts";
|
|
28
29
|
import {
|
|
@@ -213,6 +214,13 @@ export function registerCommands(pi: ExtensionAPI, rt: RuntimeState): void {
|
|
|
213
214
|
},
|
|
214
215
|
});
|
|
215
216
|
|
|
217
|
+
pi.registerCommand("studio", {
|
|
218
|
+
description: "Open the Skill Studio fullscreen workspace",
|
|
219
|
+
handler: async (_args, ctx) => {
|
|
220
|
+
await openSkillStudio(rt, ctx);
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
|
|
216
224
|
pi.registerCommand("bash-reset", {
|
|
217
225
|
description: "Reset the managed bash session",
|
|
218
226
|
handler: async (_args, ctx) => {
|
|
@@ -4,7 +4,6 @@ import { SETTING_DEFAULTS } from "../../config/settings-registry.ts";
|
|
|
4
4
|
import { PRESETS } from "../../config/presets.ts";
|
|
5
5
|
import { BashTranscriptStore } from "../../../bash-mode/transcript.ts";
|
|
6
6
|
import { BashCompletionEngine } from "../../../bash-mode/completion.ts";
|
|
7
|
-
import { ManagedShellSession } from "../../../bash-mode/shell-session.ts";
|
|
8
7
|
import { PowerlineQueueStore } from "../../../queue/store.ts";
|
|
9
8
|
import {
|
|
10
9
|
SessionBranchCache,
|
|
@@ -3,7 +3,7 @@ import type { ReadonlyFooterDataProvider } from "@earendil-works/pi-coding-agent
|
|
|
3
3
|
import type { BashModeSettings } from "../../../bash-mode/types.ts";
|
|
4
4
|
import type { BashTranscriptStore } from "../../../bash-mode/transcript.ts";
|
|
5
5
|
import type { BashCompletionEngine } from "../../../bash-mode/completion.ts";
|
|
6
|
-
import type {
|
|
6
|
+
import type { ManagedShellSessionLike } from "../../../bash-mode/types.ts";
|
|
7
7
|
import type { PowerlineQueueStore } from "../../../queue/store.ts";
|
|
8
8
|
import type {
|
|
9
9
|
SessionAssistantUsage,
|
|
@@ -74,7 +74,7 @@ export interface RuntimeState {
|
|
|
74
74
|
bashModeActive: boolean;
|
|
75
75
|
bashTranscript: BashTranscriptStore;
|
|
76
76
|
bashCompletionEngine: BashCompletionEngine;
|
|
77
|
-
shellSession:
|
|
77
|
+
shellSession: ManagedShellSessionLike | null;
|
|
78
78
|
queueStore: PowerlineQueueStore;
|
|
79
79
|
powerlineCompacting: boolean;
|
|
80
80
|
deliverAfterRetrySettles: boolean;
|
|
@@ -349,6 +349,27 @@ export function registerSessionLifecycle(
|
|
|
349
349
|
requestQueueRender(rt);
|
|
350
350
|
});
|
|
351
351
|
|
|
352
|
+
// pi 0.84.0+ emits `session_compact_failed` (not `session_compact`) when
|
|
353
|
+
// auto-compaction or /compact fails or is aborted. Without this handler
|
|
354
|
+
// `powerlineCompacting` stays true and the bar keeps showing the compact
|
|
355
|
+
// rail forever, even though the agent is idle. Root cause of the stuck
|
|
356
|
+
// "COMPACTING" seen on 2026-08-27.
|
|
357
|
+
pi.on("session_compact_failed", async (event, ctx) => {
|
|
358
|
+
rt.powerlineCompacting = false;
|
|
359
|
+
rt.deliverAfterRetrySettles = false;
|
|
360
|
+
rt.currentCtx = ctx;
|
|
361
|
+
rt.coreContextUsageCache.reset();
|
|
362
|
+
requestImmediateStatusRender(rt, { deferDuringTyping: false });
|
|
363
|
+
dispatchSignalEvent(
|
|
364
|
+
rt,
|
|
365
|
+
config.appearance,
|
|
366
|
+
event.errorMessage ? "error" : "idle",
|
|
367
|
+
event.errorMessage ? "compact failed" : undefined,
|
|
368
|
+
);
|
|
369
|
+
finishFailedCompaction(rt, ctx, event.errorMessage ?? "Compaction cancelled");
|
|
370
|
+
requestQueueRender(rt);
|
|
371
|
+
});
|
|
372
|
+
|
|
352
373
|
pi.on("agent_settled", async (_event, ctx) => {
|
|
353
374
|
if (rt.powerlineCompacting) {
|
|
354
375
|
finishFailedCompaction(rt, ctx, "Compaction did not complete");
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Writes `~/.pi/agent/skills/<name>/SKILL.md` then opens $EDITOR.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { existsSync, mkdirSync, renameSync, writeFileSync } from "node:fs";
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
import type { SelectItem } from "@earendil-works/pi-tui";
|
|
9
9
|
|
|
@@ -177,15 +177,24 @@ export function writeSkillFromTemplate(
|
|
|
177
177
|
name: string,
|
|
178
178
|
template: SkillTemplateId,
|
|
179
179
|
skillsRoot: string = getAgentPath("skills"),
|
|
180
|
+
options: { overwrite?: boolean } = {},
|
|
180
181
|
): { filePath: string } {
|
|
181
182
|
const safe = sanitizeSkillName(name);
|
|
182
183
|
const dir = join(skillsRoot, safe);
|
|
183
184
|
const filePath = join(dir, "SKILL.md");
|
|
184
|
-
if (existsSync(filePath)) {
|
|
185
|
+
if (!options.overwrite && existsSync(filePath)) {
|
|
185
186
|
throw new SkillNameError(`Skill already exists: ${safe}`);
|
|
186
187
|
}
|
|
187
188
|
mkdirSync(dir, { recursive: true });
|
|
188
|
-
|
|
189
|
+
if (options.overwrite && existsSync(filePath)) {
|
|
190
|
+
// Write the replacement next to the original and atomically rename it
|
|
191
|
+
// over SKILL.md; a failed write leaves the existing skill untouched.
|
|
192
|
+
const tmp = join(dir, `.SKILL.md.${process.pid}.tmp`);
|
|
193
|
+
writeFileSync(tmp, renderSkillTemplate(template, safe), "utf8");
|
|
194
|
+
renameSync(tmp, filePath);
|
|
195
|
+
} else {
|
|
196
|
+
writeFileSync(filePath, renderSkillTemplate(template, safe), "utf8");
|
|
197
|
+
}
|
|
189
198
|
invalidateSkillCache();
|
|
190
199
|
return { filePath };
|
|
191
200
|
}
|
|
@@ -132,6 +132,10 @@ export function setupCustomEditor(
|
|
|
132
132
|
rt.shellSession?.interrupt();
|
|
133
133
|
ctx.ui.notify("Sent interrupt to shell", "info");
|
|
134
134
|
},
|
|
135
|
+
onForwardInput: (data) => {
|
|
136
|
+
rt.shellSession?.writeStdin?.(data);
|
|
137
|
+
},
|
|
138
|
+
forwardWhileRunning: () => rt.shellSession?.supportsForwardMode() ?? false,
|
|
135
139
|
onNotify: (message, level = "info") => ctx.ui.notify(message, level),
|
|
136
140
|
getHistoryEntries: (prefix) => getShellHistoryEntries(rt, prefix),
|
|
137
141
|
resolveGhostSuggestion: async (text, signal) => {
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
import { config } from "../core/state.ts";
|
|
22
22
|
import type { RuntimeState } from "../core/types.ts";
|
|
23
23
|
import { prefersAsciiGlyphs } from "../../motion/index.ts";
|
|
24
|
-
import {
|
|
24
|
+
import { renderStatusLineV2 } from "../../render/v2-entry.ts";
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
27
|
* Get cached responsive layout or compute fresh one.
|
|
@@ -77,7 +77,7 @@ export function getResponsiveLayout(
|
|
|
77
77
|
const appearance = resolveAppearanceMix(
|
|
78
78
|
effectiveAppearanceMix(config.appearance, config.preset),
|
|
79
79
|
);
|
|
80
|
-
rt.lastLayoutResult =
|
|
80
|
+
rt.lastLayoutResult = renderStatusLineV2(
|
|
81
81
|
segmentCtx,
|
|
82
82
|
presetDef,
|
|
83
83
|
rt.signal,
|
|
@@ -182,7 +182,7 @@ export function renderBashTranscriptLines(
|
|
|
182
182
|
): string[] {
|
|
183
183
|
if (!rt.bashModeActive) return [];
|
|
184
184
|
|
|
185
|
-
const snapshot = rt.bashTranscript.
|
|
185
|
+
const snapshot = rt.bashTranscript.recentCommands(4);
|
|
186
186
|
if (snapshot.commands.length === 0) return [];
|
|
187
187
|
|
|
188
188
|
const lines: string[] = [];
|
|
@@ -192,7 +192,7 @@ export function renderBashTranscriptLines(
|
|
|
192
192
|
);
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
-
const recentCommands = snapshot.commands
|
|
195
|
+
const recentCommands = snapshot.commands;
|
|
196
196
|
for (const command of recentCommands) {
|
|
197
197
|
const promptGlyph =
|
|
198
198
|
(rt.shellSession?.state.shellName ?? "shell") === "fish" ? ">" : "$";
|
|
@@ -211,9 +211,8 @@ export function renderBashTranscriptLines(
|
|
|
211
211
|
` ${theme.fg("accent", promptGlyph)} ${commandLine} ${theme.fg("dim", "(")}${status}${theme.fg("dim", ")")}`,
|
|
212
212
|
);
|
|
213
213
|
|
|
214
|
-
|
|
215
|
-
for (const outputLine of
|
|
216
|
-
lines.push(
|
|
214
|
+
// recentCommands already tails output to the store's outputTail (6).
|
|
215
|
+
for (const outputLine of command.output) { lines.push(
|
|
217
216
|
` ${truncateToWidth(outputLine, Math.max(1, width - 3), "…")}`,
|
|
218
217
|
);
|
|
219
218
|
}
|
|
@@ -4,13 +4,31 @@ import {
|
|
|
4
4
|
discoverLoadedCounts,
|
|
5
5
|
discoverWhatsNew,
|
|
6
6
|
getRecentSessions,
|
|
7
|
+
normalizeWelcomeArt,
|
|
8
|
+
type WelcomeArtTheme,
|
|
7
9
|
} from "../../welcome/index.ts";
|
|
8
10
|
import { estimateInitialContextTokens } from "../../usage/context.ts";
|
|
9
|
-
import { isRecord } from "../settings/settings-io.ts";
|
|
11
|
+
import { isRecord, readSettings } from "../settings/settings-io.ts";
|
|
10
12
|
import type { RuntimeState } from "../core/types.ts";
|
|
11
13
|
import { getQueueContext } from "../queue/queue-context.ts";
|
|
12
14
|
import { pickNextReviewIdea } from "../queue/idea-review.ts";
|
|
13
15
|
|
|
16
|
+
interface WelcomeArtSettings {
|
|
17
|
+
art: WelcomeArtTheme;
|
|
18
|
+
animate: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Read `wishcraft.welcome.{art,animateLantern}` from settings.json. */
|
|
22
|
+
function readWelcomeArtSettings(ctx: any): WelcomeArtSettings {
|
|
23
|
+
const settings = readSettings(ctx.cwd ?? process.cwd());
|
|
24
|
+
const wishcraft = isRecord(settings.wishcraft) ? settings.wishcraft : {};
|
|
25
|
+
const welcome = isRecord(wishcraft.welcome) ? wishcraft.welcome : {};
|
|
26
|
+
return {
|
|
27
|
+
art: normalizeWelcomeArt(welcome.art),
|
|
28
|
+
animate: welcome.animateLantern === true,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
14
32
|
export function setupWelcomeHeader(rt: RuntimeState, ctx: any) {
|
|
15
33
|
const modelName = ctx.model?.name || ctx.model?.id || "No model";
|
|
16
34
|
const providerName = ctx.model?.provider || "Unknown";
|
|
@@ -37,6 +55,8 @@ export function setupWelcomeHeader(rt: RuntimeState, ctx: any) {
|
|
|
37
55
|
whatsNew,
|
|
38
56
|
nextIdeaText,
|
|
39
57
|
);
|
|
58
|
+
const artSettings = readWelcomeArtSettings(ctx);
|
|
59
|
+
header.setArt(artSettings.art, artSettings.animate);
|
|
40
60
|
rt.welcomeHeaderActive = true;
|
|
41
61
|
|
|
42
62
|
ctx.ui.setHeader(() => {
|
|
@@ -115,6 +135,8 @@ export function setupWelcomeOverlay(rt: RuntimeState, ctx: any) {
|
|
|
115
135
|
whatsNew,
|
|
116
136
|
nextIdeaText,
|
|
117
137
|
);
|
|
138
|
+
const artSettings = readWelcomeArtSettings(ctx);
|
|
139
|
+
welcome.setArt(artSettings.art, artSettings.animate);
|
|
118
140
|
|
|
119
141
|
let countdown = 30;
|
|
120
142
|
let dismissed = false;
|
package/src/motion/frames.ts
CHANGED
|
@@ -16,8 +16,10 @@ const GEOMETRY_FRAMES: Record<string, string[]> = {
|
|
|
16
16
|
ember: ["◇", "◈", "◆", "◈"],
|
|
17
17
|
orbit: ["◜", "◝", "◞", "◟"],
|
|
18
18
|
bloom: ["·", "◇", "◈", "◆", "◈", "◇"],
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
// Density-pulse families (box-drawing, één glyphsysteem — shade-blokken
|
|
20
|
+
// zijn bewust weg: drie families door elkaar las als modder).
|
|
21
|
+
heat: ["─", "╌", "╾", "━", "╾", "╌"],
|
|
22
|
+
liquid: ["·", "╌", "╾", "━"],
|
|
21
23
|
stitch: ["·", "╼", "◆", "╾"],
|
|
22
24
|
refract: ["╭", "╮", "╯", "╰"],
|
|
23
25
|
write: ["─", "──", "───", "────╾"],
|
|
@@ -73,12 +75,20 @@ export function sweepPosition(
|
|
|
73
75
|
return direction === "reverse" ? width - 1 - step : step;
|
|
74
76
|
}
|
|
75
77
|
|
|
76
|
-
/**
|
|
78
|
+
/**
|
|
79
|
+
* Trail glyph by distance behind the travelling head. One family:
|
|
80
|
+
* box-drawing density fading back to the light track — no shade blocks.
|
|
81
|
+
*/
|
|
77
82
|
export function trailGlyph(distance: number, ascii = false): string {
|
|
78
|
-
if (ascii)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
83
|
+
if (ascii) {
|
|
84
|
+
if (distance === 0) return "*";
|
|
85
|
+
if (distance === 1) return "=";
|
|
86
|
+
if (distance === 2) return ">";
|
|
87
|
+
return "-";
|
|
88
|
+
}
|
|
89
|
+
if (distance === 0) return "●";
|
|
90
|
+
if (distance === 1) return "━";
|
|
91
|
+
if (distance === 2) return "╾";
|
|
92
|
+
if (distance === 3) return "╌";
|
|
93
|
+
return "─";
|
|
84
94
|
}
|