@aliou/pi-processes 0.9.5 → 0.10.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/CONTRIBUTING.md +40 -42
- package/README.md +54 -84
- package/extensions/process-tabs.ts +26 -0
- package/extensions/processes/client.ts +194 -0
- package/extensions/processes/commands/clear.ts +29 -0
- package/extensions/processes/commands/kill.ts +152 -0
- package/extensions/processes/commands/overview.ts +102 -0
- package/extensions/processes/components/overview-component.ts +729 -0
- package/extensions/processes/components/overview-panel.ts +177 -0
- package/extensions/processes/config/defaults.ts +28 -0
- package/extensions/processes/config/index.ts +13 -0
- package/extensions/processes/config/loader.ts +62 -0
- package/extensions/processes/config/migrations/000-import-legacy-process-config.ts +113 -0
- package/extensions/processes/config/migrations/001-v0-9-4-to-v0-10-0-config.ts +184 -0
- package/extensions/processes/config/migrations/002-stamp-v0-10-0-config-version.ts +21 -0
- package/extensions/processes/config/migrations/index.ts +25 -0
- package/extensions/processes/config/schema.ts +9 -0
- package/extensions/processes/config/types.ts +72 -0
- package/extensions/processes/constants.ts +1 -0
- package/extensions/processes/handlers/commands.ts +113 -0
- package/extensions/processes/handlers/kill-process.ts +31 -0
- package/extensions/processes/handlers/notifications.ts +147 -0
- package/extensions/processes/handlers/requests.ts +114 -0
- package/extensions/processes/handlers/subscriptions.ts +144 -0
- package/extensions/processes/hooks/background-blocker.ts +201 -0
- package/extensions/processes/hooks/cleanup.ts +43 -0
- package/extensions/processes/hooks/event-bridge.ts +32 -0
- package/extensions/processes/i18n/index.ts +3 -0
- package/extensions/processes/i18n/messages.ts +27 -0
- package/extensions/processes/i18n/translator.ts +49 -0
- package/extensions/processes/index.ts +109 -0
- package/extensions/processes/message-renderer.ts +117 -0
- package/extensions/processes/notification-sender.ts +43 -0
- package/extensions/processes/notifications/classify.ts +27 -0
- package/extensions/processes/notifications/log-matchers.ts +146 -0
- package/extensions/processes/notifications/registry.ts +225 -0
- package/extensions/processes/notifications/render-content.ts +122 -0
- package/extensions/processes/notifications/service.ts +336 -0
- package/extensions/processes/notifications/types.ts +11 -0
- package/extensions/processes/settings/apply-setting-change.ts +30 -0
- package/extensions/processes/settings/build-sections.ts +420 -0
- package/extensions/processes/settings/index.ts +42 -0
- package/extensions/processes/tools/clear/index.ts +21 -0
- package/extensions/processes/tools/clear/render.ts +40 -0
- package/extensions/processes/tools/components/index.ts +4 -0
- package/extensions/processes/tools/components/process-action-header.ts +31 -0
- package/extensions/processes/tools/components/process-action-title.ts +17 -0
- package/extensions/processes/tools/components/tool-layout.ts +70 -0
- package/extensions/processes/tools/components/watch.ts +53 -0
- package/extensions/processes/tools/index.ts +265 -0
- package/extensions/processes/tools/list/index.ts +157 -0
- package/extensions/processes/tools/list/render.ts +211 -0
- package/extensions/processes/tools/notify.ts +191 -0
- package/extensions/processes/tools/output/index.ts +307 -0
- package/extensions/processes/tools/output/render.ts +229 -0
- package/extensions/processes/tools/schema.ts +263 -0
- package/extensions/processes/tools/start/index.ts +64 -0
- package/extensions/processes/tools/start/render.ts +67 -0
- package/extensions/processes/tools/stop/index.ts +35 -0
- package/extensions/processes/tools/stop/render.ts +43 -0
- package/extensions/processes/tools/update/index.ts +320 -0
- package/extensions/processes/tools/update/render.ts +251 -0
- package/extensions/processes/tools/utils.ts +112 -0
- package/extensions/processes/tools/watch-format.ts +14 -0
- package/extensions/processes/tools/write/index.ts +75 -0
- package/extensions/processes/tools/write/render.ts +68 -0
- package/extensions/processes/utils/truncate.ts +285 -0
- package/extensions/processes-dock/client.ts +60 -0
- package/extensions/processes-dock/commands/dock.ts +70 -0
- package/extensions/processes-dock/commands/pin.ts +166 -0
- package/extensions/processes-dock/components/log-dock-component.ts +375 -0
- package/extensions/processes-dock/dock-state.ts +33 -0
- package/extensions/processes-dock/index.ts +28 -0
- package/extensions/processes-dock/logs-client.ts +5 -0
- package/extensions/processes-dock/widget/setup.ts +520 -0
- package/extensions/processes-dock/widget/status.ts +149 -0
- package/extensions/processes-dock/widget/types.ts +16 -0
- package/extensions/processes-logs/client.ts +51 -0
- package/extensions/processes-logs/commands/logs.ts +108 -0
- package/extensions/processes-logs/completions.ts +31 -0
- package/extensions/processes-logs/components/log-file-viewer.ts +274 -0
- package/extensions/processes-logs/components/log-overlay-component.ts +685 -0
- package/extensions/processes-logs/index.ts +27 -0
- package/extensions/processes-logs/logs-client.ts +101 -0
- package/extensions/shared/line-buffer.ts +35 -0
- package/extensions/shared/output-payload.ts +28 -0
- package/extensions/shared/ui.ts +146 -0
- package/package.json +34 -9
- package/schema.json +134 -0
- package/skills/pi-processes/SKILL.md +496 -25
- package/src/get-manager.ts +15 -0
- package/src/manager/index.ts +182 -0
- package/src/manager/internal-types.ts +130 -0
- package/src/manager/limits.ts +15 -0
- package/src/manager/process-log-store.ts +422 -0
- package/src/manager/process-output.ts +345 -0
- package/src/manager/process-registry.ts +78 -0
- package/src/manager/process-runtime-controller.ts +499 -0
- package/src/protocol/broadcasts.ts +14 -0
- package/src/protocol/channels.ts +30 -0
- package/src/protocol/commands.ts +24 -0
- package/src/protocol/index.ts +34 -0
- package/src/protocol/logs.ts +23 -0
- package/src/protocol/notifications.ts +48 -0
- package/src/protocol/requests.ts +76 -0
- package/src/types.ts +66 -0
- package/src/utils/ansi.ts +2 -4
- package/src/utils/format.ts +24 -32
- package/src/utils/index.ts +5 -1
- package/src/utils/is-record.ts +3 -0
- package/src/utils/match-line.ts +32 -0
- package/src/utils/platform.ts +13 -0
- package/src/utils/shorten-path.ts +11 -0
- package/src/utils/signals.ts +22 -0
- package/src/commands/clear/command.ts +0 -14
- package/src/commands/clear/index.ts +0 -1
- package/src/commands/completions.ts +0 -38
- package/src/commands/dock/command.ts +0 -29
- package/src/commands/dock/index.ts +0 -1
- package/src/commands/index.ts +0 -33
- package/src/commands/kill/command.ts +0 -69
- package/src/commands/kill/index.ts +0 -1
- package/src/commands/logs/command.ts +0 -47
- package/src/commands/logs/index.ts +0 -1
- package/src/commands/pick-process.ts +0 -34
- package/src/commands/pin/command.ts +0 -34
- package/src/commands/pin/index.ts +0 -1
- package/src/commands/processes/command.ts +0 -39
- package/src/commands/processes/index.ts +0 -1
- package/src/commands/settings/apply-setting-change.ts +0 -72
- package/src/commands/settings/build-sections.ts +0 -160
- package/src/commands/settings/command.ts +0 -20
- package/src/commands/settings/index.ts +0 -1
- package/src/components/log-dock-component.test.ts +0 -29
- package/src/components/log-dock-component.ts +0 -236
- package/src/components/log-file-viewer.ts +0 -317
- package/src/components/log-overlay-component.ts +0 -546
- package/src/components/panel-helpers.ts +0 -37
- package/src/components/process-picker-component.ts +0 -166
- package/src/components/processes-component.ts +0 -485
- package/src/components/status-format.ts +0 -38
- package/src/config.ts +0 -107
- package/src/constants/index.ts +0 -16
- package/src/constants/types.ts +0 -119
- package/src/hooks/background-blocker.ts +0 -66
- package/src/hooks/cleanup.ts +0 -10
- package/src/hooks/index.ts +0 -32
- package/src/hooks/message-renderer.ts +0 -115
- package/src/hooks/process-end.ts +0 -69
- package/src/hooks/process-watch.ts +0 -85
- package/src/hooks/utils.ts +0 -23
- package/src/hooks/widget/index.ts +0 -2
- package/src/hooks/widget/setup.ts +0 -167
- package/src/hooks/widget/status-widget.ts +0 -105
- package/src/hooks/widget/types.ts +0 -20
- package/src/index.ts +0 -35
- package/src/manager.test.ts +0 -331
- package/src/manager.ts +0 -758
- package/src/tools/actions/clear.ts +0 -20
- package/src/tools/actions/debug.ts +0 -155
- package/src/tools/actions/index.ts +0 -171
- package/src/tools/actions/kill.ts +0 -89
- package/src/tools/actions/list.ts +0 -188
- package/src/tools/actions/logs.ts +0 -117
- package/src/tools/actions/output-truncate.ts +0 -187
- package/src/tools/actions/output.test.ts +0 -392
- package/src/tools/actions/output.ts +0 -447
- package/src/tools/actions/start.ts +0 -245
- package/src/tools/actions/write.ts +0 -113
- package/src/tools/index.ts +0 -207
- package/src/utils/ansi.test.ts +0 -22
- package/src/utils/command-executor.test.ts +0 -48
- package/src/utils/keybindings.ts +0 -71
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type EventBus,
|
|
3
|
+
type ExtensionAPI,
|
|
4
|
+
type ExtensionCommandContext,
|
|
5
|
+
getSelectListTheme,
|
|
6
|
+
} from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import {
|
|
8
|
+
type AutocompleteItem,
|
|
9
|
+
type SelectItem,
|
|
10
|
+
SelectList,
|
|
11
|
+
} from "@earendil-works/pi-tui";
|
|
12
|
+
import { formatProcessSelectionDescription } from "../../shared/ui";
|
|
13
|
+
import { requestProcess, requestProcessList } from "../client";
|
|
14
|
+
import type { DockController } from "../widget/setup";
|
|
15
|
+
|
|
16
|
+
export function registerPinCommand(
|
|
17
|
+
pi: Pick<ExtensionAPI, "registerCommand">,
|
|
18
|
+
events: EventBus,
|
|
19
|
+
getController: () => DockController | null,
|
|
20
|
+
): void {
|
|
21
|
+
pi.registerCommand("ps:pin", {
|
|
22
|
+
description: "Pin the process dock to a process.",
|
|
23
|
+
getArgumentCompletions: (prefix: string) =>
|
|
24
|
+
completions(
|
|
25
|
+
events,
|
|
26
|
+
prefix,
|
|
27
|
+
getController()?.actions.getFocusedProcessId() ?? null,
|
|
28
|
+
),
|
|
29
|
+
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
30
|
+
const controller = getController();
|
|
31
|
+
if (!controller) {
|
|
32
|
+
report(ctx, "Process dock is only available in UI mode.", "warning");
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const rawId = args.trim().split(/\s+/, 1)[0];
|
|
37
|
+
const pickedId = rawId
|
|
38
|
+
? rawId
|
|
39
|
+
: await pickPinTarget(ctx, events, controller);
|
|
40
|
+
if (!pickedId) return;
|
|
41
|
+
const id = pickedId;
|
|
42
|
+
|
|
43
|
+
if (id === "clear") {
|
|
44
|
+
controller.actions.setFocus(null);
|
|
45
|
+
controller.actions.expand();
|
|
46
|
+
controller.refresh();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const process = requestProcess(events, id);
|
|
51
|
+
if (!process) {
|
|
52
|
+
report(ctx, `Process not found: ${id}`, "warning");
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
controller.actions.setFocus(process.id);
|
|
57
|
+
controller.actions.expand();
|
|
58
|
+
controller.refresh();
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function pickPinTarget(
|
|
64
|
+
ctx: ExtensionCommandContext,
|
|
65
|
+
events: EventBus,
|
|
66
|
+
controller: DockController,
|
|
67
|
+
): Promise<string | null> {
|
|
68
|
+
if (ctx.mode !== "tui") {
|
|
69
|
+
report(ctx, "Usage: /ps:pin <process-id|clear>", "warning");
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const pinnedProcessId = controller.actions.getFocusedProcessId();
|
|
74
|
+
const processes = requestProcessList(events);
|
|
75
|
+
const items: SelectItem[] = [
|
|
76
|
+
...(pinnedProcessId
|
|
77
|
+
? [
|
|
78
|
+
{
|
|
79
|
+
label: "clear",
|
|
80
|
+
value: "clear",
|
|
81
|
+
description: "Unpin the dock and show all running process logs.",
|
|
82
|
+
},
|
|
83
|
+
]
|
|
84
|
+
: []),
|
|
85
|
+
...processes.map((process) => ({
|
|
86
|
+
label: `${process.name} (${process.id})`,
|
|
87
|
+
value: process.id,
|
|
88
|
+
description: formatProcessSelectionDescription(
|
|
89
|
+
process,
|
|
90
|
+
process.id === pinnedProcessId ? " — pinned" : "",
|
|
91
|
+
),
|
|
92
|
+
})),
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
if (items.length === 0) {
|
|
96
|
+
report(ctx, "No processes to pin.", "warning");
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const result = await ctx.ui.custom<string | null>(
|
|
101
|
+
(tui, theme, _keybindings, done) => {
|
|
102
|
+
const list = new SelectList(
|
|
103
|
+
items,
|
|
104
|
+
Math.min(items.length, 10),
|
|
105
|
+
getSelectListTheme(),
|
|
106
|
+
);
|
|
107
|
+
list.onSelect = (item) => done(String(item.value));
|
|
108
|
+
list.onCancel = () => done(null);
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
render: (width: number) => [
|
|
112
|
+
theme.fg("muted", "─".repeat(Math.max(0, width))),
|
|
113
|
+
...list.render(width),
|
|
114
|
+
theme.fg("muted", "─".repeat(Math.max(0, width))),
|
|
115
|
+
],
|
|
116
|
+
invalidate: () => list.invalidate(),
|
|
117
|
+
handleInput: (data: string) => {
|
|
118
|
+
list.handleInput(data);
|
|
119
|
+
tui.requestRender();
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
},
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
return result ?? null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function completions(
|
|
129
|
+
events: EventBus,
|
|
130
|
+
prefix: string,
|
|
131
|
+
pinnedProcessId: string | null,
|
|
132
|
+
): AutocompleteItem[] | null {
|
|
133
|
+
const normalized = prefix.trim().toLowerCase();
|
|
134
|
+
const clearItems =
|
|
135
|
+
pinnedProcessId && "clear".startsWith(normalized)
|
|
136
|
+
? [
|
|
137
|
+
{
|
|
138
|
+
value: "clear",
|
|
139
|
+
label: "clear",
|
|
140
|
+
description: "Unpin the dock and show all running process logs.",
|
|
141
|
+
},
|
|
142
|
+
]
|
|
143
|
+
: [];
|
|
144
|
+
const processItems = requestProcessList(events)
|
|
145
|
+
.filter(
|
|
146
|
+
(process) =>
|
|
147
|
+
process.id.toLowerCase().startsWith(normalized) ||
|
|
148
|
+
process.name.toLowerCase().includes(normalized),
|
|
149
|
+
)
|
|
150
|
+
.map((process) => ({
|
|
151
|
+
value: process.id,
|
|
152
|
+
label: `${process.name} (${process.id})`,
|
|
153
|
+
description: formatProcessSelectionDescription(process),
|
|
154
|
+
}));
|
|
155
|
+
const items = [...clearItems, ...processItems];
|
|
156
|
+
return items.length > 0 ? items : null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function report(
|
|
160
|
+
ctx: ExtensionCommandContext,
|
|
161
|
+
message: string,
|
|
162
|
+
level: "info" | "warning",
|
|
163
|
+
): void {
|
|
164
|
+
if (ctx.hasUI) ctx.ui.notify(message, level);
|
|
165
|
+
else console.log(message);
|
|
166
|
+
}
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import { Stack } from "@aliou/pi-utils-ui";
|
|
2
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import {
|
|
4
|
+
type Component,
|
|
5
|
+
truncateToWidth,
|
|
6
|
+
visibleWidth,
|
|
7
|
+
} from "@earendil-works/pi-tui";
|
|
8
|
+
import type { ProcessInfo } from "../../../src/types";
|
|
9
|
+
import { LIVE_STATUSES } from "../../../src/types";
|
|
10
|
+
import { stripAnsi } from "../../../src/utils/ansi";
|
|
11
|
+
import { renderProcessTab } from "../../process-tabs";
|
|
12
|
+
import {
|
|
13
|
+
clampNameColumn,
|
|
14
|
+
LineComponent,
|
|
15
|
+
LinesComponent,
|
|
16
|
+
statusDot,
|
|
17
|
+
} from "../../shared/ui";
|
|
18
|
+
import type { ProcessLogLine } from "../client";
|
|
19
|
+
import type { DockState } from "../widget/types";
|
|
20
|
+
|
|
21
|
+
export interface ProcessLogEntry {
|
|
22
|
+
processId: string;
|
|
23
|
+
line: ProcessLogLine;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface LogDockSnapshot {
|
|
27
|
+
processes: ProcessInfo[];
|
|
28
|
+
pinnedProcess: ProcessInfo | null;
|
|
29
|
+
pinnedLines: ProcessLogLine[];
|
|
30
|
+
processLogStream: ProcessLogEntry[];
|
|
31
|
+
previews: Map<string, ProcessLogLine | null>;
|
|
32
|
+
notifyLines: Set<string>;
|
|
33
|
+
notifyCounts: Map<string, number>;
|
|
34
|
+
state: DockState;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function renderCollapsedDockLine(
|
|
38
|
+
content: string,
|
|
39
|
+
width: number,
|
|
40
|
+
): string {
|
|
41
|
+
if (width <= 0) return "";
|
|
42
|
+
|
|
43
|
+
const line = truncateToWidth(content, width, "", true);
|
|
44
|
+
return `${line}${" ".repeat(Math.max(0, width - visibleWidth(line)))}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function renderLogDock(
|
|
48
|
+
snapshot: LogDockSnapshot,
|
|
49
|
+
theme: Theme,
|
|
50
|
+
width: number,
|
|
51
|
+
logRows: number,
|
|
52
|
+
): string[] {
|
|
53
|
+
if (snapshot.state.visibility === "closed") return [];
|
|
54
|
+
if (snapshot.processes.length === 0) return [];
|
|
55
|
+
|
|
56
|
+
const body =
|
|
57
|
+
snapshot.state.visibility === "expanded"
|
|
58
|
+
? buildExpandedBody(snapshot, theme, Math.max(1, logRows))
|
|
59
|
+
: buildCollapsedBody(snapshot, theme);
|
|
60
|
+
|
|
61
|
+
return renderMinimalBox(
|
|
62
|
+
"Processes",
|
|
63
|
+
body,
|
|
64
|
+
theme,
|
|
65
|
+
width,
|
|
66
|
+
snapshot.state.visibility === "expanded",
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const SIDE_PADDING = 1;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Render the dock as a minimal panel: a top bar with the title (no side
|
|
74
|
+
* padding), an optional full-width rule between the process strip and the
|
|
75
|
+
* logs, and a single space of side padding for the body lines. No bottom
|
|
76
|
+
* border so the dock opens cleanly into the editor below.
|
|
77
|
+
*/
|
|
78
|
+
function renderMinimalBox(
|
|
79
|
+
title: string,
|
|
80
|
+
body: Component,
|
|
81
|
+
theme: Theme,
|
|
82
|
+
width: number,
|
|
83
|
+
withProcessStrip: boolean,
|
|
84
|
+
): string[] {
|
|
85
|
+
const dim = (text: string) => theme.fg("dim", text);
|
|
86
|
+
const lines: string[] = [];
|
|
87
|
+
|
|
88
|
+
// Top bar: no side padding, title sits after a small "── " flourish.
|
|
89
|
+
const label = `── ${title} `;
|
|
90
|
+
const labelWidth = visibleWidth(label);
|
|
91
|
+
if (labelWidth >= width) {
|
|
92
|
+
lines.push(dim("─".repeat(Math.max(0, width))));
|
|
93
|
+
} else {
|
|
94
|
+
lines.push(dim(label) + dim("─".repeat(width - labelWidth)));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const innerWidth = Math.max(0, width - SIDE_PADDING * 2);
|
|
98
|
+
const pad = " ".repeat(SIDE_PADDING);
|
|
99
|
+
const bodyLines = body.render(innerWidth);
|
|
100
|
+
|
|
101
|
+
if (!withProcessStrip) {
|
|
102
|
+
// Collapsed mode: body is just log preview lines.
|
|
103
|
+
for (const line of bodyLines) {
|
|
104
|
+
const log = truncateToWidth(line, innerWidth, "", true);
|
|
105
|
+
lines.push(`${pad}${log}${pad}`);
|
|
106
|
+
}
|
|
107
|
+
return lines;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Expanded mode: first body line is the process strip, the rest are logs.
|
|
111
|
+
if (bodyLines.length > 0) {
|
|
112
|
+
const strip = truncateToWidth(bodyLines[0], innerWidth, "", true);
|
|
113
|
+
lines.push(`${pad}${strip}${pad}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (bodyLines.length > 1) {
|
|
117
|
+
lines.push(dim("─".repeat(Math.max(0, width))));
|
|
118
|
+
for (let i = 1; i < bodyLines.length; i++) {
|
|
119
|
+
const log = truncateToWidth(bodyLines[i], innerWidth, "", true);
|
|
120
|
+
lines.push(`${pad}${log}${pad}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return lines;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function buildCollapsedBody(
|
|
128
|
+
snapshot: LogDockSnapshot,
|
|
129
|
+
theme: Theme,
|
|
130
|
+
): Component {
|
|
131
|
+
// Collapsed dock is intentionally minimal: just the header and a short
|
|
132
|
+
// log preview. No process strip.
|
|
133
|
+
return new LinesComponent((width) =>
|
|
134
|
+
renderCollapsedLogLines(snapshot, theme, width),
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function buildExpandedBody(
|
|
139
|
+
snapshot: LogDockSnapshot,
|
|
140
|
+
theme: Theme,
|
|
141
|
+
logRows: number,
|
|
142
|
+
): Component {
|
|
143
|
+
const body = new Stack({ gap: 0 });
|
|
144
|
+
body.addChild(
|
|
145
|
+
new LineComponent((width) => renderProcessStrip(snapshot, theme, width)),
|
|
146
|
+
);
|
|
147
|
+
body.addChild(
|
|
148
|
+
new LinesComponent((width) => {
|
|
149
|
+
const contentLines = snapshot.pinnedProcess
|
|
150
|
+
? renderPinnedLogLines(snapshot, theme, width, logRows)
|
|
151
|
+
: renderAllRunningLogLines(snapshot, theme, width, logRows);
|
|
152
|
+
return Array.from(
|
|
153
|
+
{ length: logRows },
|
|
154
|
+
(_, index) => contentLines[index] ?? "",
|
|
155
|
+
);
|
|
156
|
+
}),
|
|
157
|
+
);
|
|
158
|
+
return body;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function renderCollapsedLogLines(
|
|
162
|
+
snapshot: LogDockSnapshot,
|
|
163
|
+
theme: Theme,
|
|
164
|
+
width: number,
|
|
165
|
+
): string[] {
|
|
166
|
+
const height = 2;
|
|
167
|
+
const lines = snapshot.pinnedProcess
|
|
168
|
+
? renderCollapsedPinnedLogLines(snapshot, theme, width, height)
|
|
169
|
+
: renderAllRunningLogLines(snapshot, theme, width, height);
|
|
170
|
+
|
|
171
|
+
if (lines.length === 0) return ["", ""];
|
|
172
|
+
return Array.from({ length: height }, (_, index) => lines[index] ?? "");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function renderCollapsedPinnedLogLines(
|
|
176
|
+
snapshot: LogDockSnapshot,
|
|
177
|
+
theme: Theme,
|
|
178
|
+
width: number,
|
|
179
|
+
height: number,
|
|
180
|
+
): string[] {
|
|
181
|
+
if (snapshot.pinnedLines.length > 0) {
|
|
182
|
+
return renderLogLines(snapshot.pinnedLines, snapshot, theme, width, height);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const processId = snapshot.pinnedProcess?.id;
|
|
186
|
+
const lines = processId
|
|
187
|
+
? snapshot.processLogStream
|
|
188
|
+
.filter((entry) => entry.processId === processId)
|
|
189
|
+
.map((entry) => entry.line)
|
|
190
|
+
: [];
|
|
191
|
+
return renderLogLines(lines, snapshot, theme, width, height);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function renderProcessStrip(
|
|
195
|
+
snapshot: LogDockSnapshot,
|
|
196
|
+
theme: Theme,
|
|
197
|
+
width: number,
|
|
198
|
+
): string {
|
|
199
|
+
const liveOrVisible = snapshot.processes.filter(
|
|
200
|
+
(process) =>
|
|
201
|
+
LIVE_STATUSES.has(process.status) ||
|
|
202
|
+
snapshot.pinnedProcess?.id === process.id,
|
|
203
|
+
);
|
|
204
|
+
const finished = snapshot.processes.filter(
|
|
205
|
+
(process) =>
|
|
206
|
+
!LIVE_STATUSES.has(process.status) &&
|
|
207
|
+
snapshot.pinnedProcess?.id !== process.id,
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
// Show live and failed/killed processes individually; collapse only
|
|
211
|
+
// exited-success into a single summary token.
|
|
212
|
+
const exitedSuccess: ProcessInfo[] = [];
|
|
213
|
+
const shown: ProcessInfo[] = [];
|
|
214
|
+
for (const process of finished) {
|
|
215
|
+
if (process.status === "exited" && process.success) {
|
|
216
|
+
exitedSuccess.push(process);
|
|
217
|
+
} else {
|
|
218
|
+
shown.push(process);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const ordered = [...liveOrVisible, ...shown];
|
|
223
|
+
const parts = ordered.map((process) =>
|
|
224
|
+
renderProcessToken(process, snapshot, theme),
|
|
225
|
+
);
|
|
226
|
+
if (exitedSuccess.length > 0) {
|
|
227
|
+
const summary = {
|
|
228
|
+
id: "_summary",
|
|
229
|
+
name: "",
|
|
230
|
+
status: "exited",
|
|
231
|
+
success: true,
|
|
232
|
+
exitCode: 0,
|
|
233
|
+
} as ProcessInfo;
|
|
234
|
+
parts.push(
|
|
235
|
+
`${statusDot(summary, false, theme)} ${theme.fg("dim", `${exitedSuccess.length} done`)}`,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return truncateToWidth(
|
|
240
|
+
parts.join(" ") || theme.fg("dim", "No running processes"),
|
|
241
|
+
width,
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function renderPinnedLogLines(
|
|
246
|
+
snapshot: LogDockSnapshot,
|
|
247
|
+
theme: Theme,
|
|
248
|
+
width: number,
|
|
249
|
+
height: number,
|
|
250
|
+
): string[] {
|
|
251
|
+
if (snapshot.pinnedLines.length === 0) {
|
|
252
|
+
return centerLine(theme.fg("muted", "No output yet"), width, height);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return renderLogLines(snapshot.pinnedLines, snapshot, theme, width, height);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function renderAllRunningLogLines(
|
|
259
|
+
snapshot: LogDockSnapshot,
|
|
260
|
+
theme: Theme,
|
|
261
|
+
width: number,
|
|
262
|
+
height: number,
|
|
263
|
+
): string[] {
|
|
264
|
+
const byId = new Map(
|
|
265
|
+
snapshot.processes.map((process) => [process.id, process]),
|
|
266
|
+
);
|
|
267
|
+
const stream = snapshot.processLogStream.filter(({ processId }) => {
|
|
268
|
+
const process = byId.get(processId);
|
|
269
|
+
return process ? LIVE_STATUSES.has(process.status) : false;
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
if (stream.length === 0) {
|
|
273
|
+
return centerLine(theme.fg("muted", "No output yet"), width, height);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const activeProcesses = snapshot.processes.filter((process) =>
|
|
277
|
+
LIVE_STATUSES.has(process.status),
|
|
278
|
+
);
|
|
279
|
+
const nameCol = clampNameColumn(activeProcesses);
|
|
280
|
+
const separator = theme.fg("dim", " │ ");
|
|
281
|
+
const sepLen = visibleWidth(separator);
|
|
282
|
+
|
|
283
|
+
return stream.slice(-height).map(({ processId, line }) => {
|
|
284
|
+
const process = byId.get(processId);
|
|
285
|
+
const label = theme.fg("dim", padName(process?.name ?? processId, nameCol));
|
|
286
|
+
const text = renderLogText(
|
|
287
|
+
line,
|
|
288
|
+
snapshot,
|
|
289
|
+
theme,
|
|
290
|
+
Math.max(1, width - nameCol - sepLen),
|
|
291
|
+
);
|
|
292
|
+
return truncateToWidth(`${label}${separator}${text}`, width, "", true);
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function renderLogLines(
|
|
297
|
+
lines: ProcessLogLine[],
|
|
298
|
+
snapshot: LogDockSnapshot,
|
|
299
|
+
theme: Theme,
|
|
300
|
+
width: number,
|
|
301
|
+
height: number,
|
|
302
|
+
): string[] {
|
|
303
|
+
const compacted = compactRepeatedLines(lines);
|
|
304
|
+
return compacted
|
|
305
|
+
.slice(-height)
|
|
306
|
+
.map((line) => renderLogText(line, snapshot, theme, width));
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function renderLogText(
|
|
310
|
+
line: ProcessLogLine,
|
|
311
|
+
snapshot: LogDockSnapshot,
|
|
312
|
+
theme: Theme,
|
|
313
|
+
width: number,
|
|
314
|
+
): string {
|
|
315
|
+
const text = truncateToWidth(stripAnsi(line.text), width, "", true);
|
|
316
|
+
if (snapshot.notifyLines.has(line.text) || snapshot.notifyLines.has(text)) {
|
|
317
|
+
return truncateToWidth(theme.underline(text), width);
|
|
318
|
+
}
|
|
319
|
+
if (line.type === "stderr") {
|
|
320
|
+
return truncateToWidth(theme.fg("warning", text), width);
|
|
321
|
+
}
|
|
322
|
+
return truncateToWidth(text, width);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function renderProcessToken(
|
|
326
|
+
process: ProcessInfo,
|
|
327
|
+
snapshot: LogDockSnapshot,
|
|
328
|
+
theme: Theme,
|
|
329
|
+
): string {
|
|
330
|
+
const tab = renderProcessTab(
|
|
331
|
+
process,
|
|
332
|
+
snapshot.pinnedProcess?.id === process.id,
|
|
333
|
+
theme,
|
|
334
|
+
);
|
|
335
|
+
const badge = snapshot.notifyCounts.get(process.id)
|
|
336
|
+
? theme.fg("warning", `▸${snapshot.notifyCounts.get(process.id)}`)
|
|
337
|
+
: "";
|
|
338
|
+
return badge ? `${tab}${badge}` : tab;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function compactRepeatedLines(lines: ProcessLogLine[]): ProcessLogLine[] {
|
|
342
|
+
const result: ProcessLogLine[] = [];
|
|
343
|
+
for (let index = 0; index < lines.length; index++) {
|
|
344
|
+
const line = lines[index];
|
|
345
|
+
let repeats = 1;
|
|
346
|
+
while (
|
|
347
|
+
index + repeats < lines.length &&
|
|
348
|
+
lines[index + repeats]?.type === line.type &&
|
|
349
|
+
lines[index + repeats]?.text === line.text
|
|
350
|
+
) {
|
|
351
|
+
repeats++;
|
|
352
|
+
}
|
|
353
|
+
result.push(line);
|
|
354
|
+
if (repeats >= 3) {
|
|
355
|
+
result.push({ type: line.type, text: `… repeated ${repeats - 1} times` });
|
|
356
|
+
} else {
|
|
357
|
+
for (let repeat = 1; repeat < repeats; repeat++) result.push(line);
|
|
358
|
+
}
|
|
359
|
+
index += repeats - 1;
|
|
360
|
+
}
|
|
361
|
+
return result;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function centerLine(content: string, width: number, height: number): string[] {
|
|
365
|
+
const lines = Array.from({ length: height }, () => "");
|
|
366
|
+
const row = Math.max(0, Math.floor((height - 1) / 2));
|
|
367
|
+
const leftPad = Math.max(0, Math.floor((width - visibleWidth(content)) / 2));
|
|
368
|
+
lines[row] = truncateToWidth(`${" ".repeat(leftPad)}${content}`, width);
|
|
369
|
+
return lines;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function padName(value: string, width: number): string {
|
|
373
|
+
const name = truncateToWidth(value, width, "", true);
|
|
374
|
+
return `${name}${" ".repeat(Math.max(0, width - visibleWidth(name)))}`;
|
|
375
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { DockActions, DockState, DockVisibility } from "./widget/types";
|
|
2
|
+
|
|
3
|
+
export function createDockState(initial: Partial<DockState> = {}): {
|
|
4
|
+
getState: () => DockState;
|
|
5
|
+
actions: DockActions;
|
|
6
|
+
} {
|
|
7
|
+
const state: DockState = {
|
|
8
|
+
visibility: initial.visibility ?? "closed",
|
|
9
|
+
followEnabled: initial.followEnabled ?? true,
|
|
10
|
+
focusedProcessId: initial.focusedProcessId ?? null,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const setVisibility = (visibility: DockVisibility) => {
|
|
14
|
+
state.visibility = visibility;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
getState: () => ({ ...state }),
|
|
19
|
+
actions: {
|
|
20
|
+
getFocusedProcessId: () => state.focusedProcessId,
|
|
21
|
+
isFollowEnabled: () => state.followEnabled,
|
|
22
|
+
setFocus: (processId) => {
|
|
23
|
+
state.focusedProcessId = processId;
|
|
24
|
+
if (processId && state.visibility === "closed") {
|
|
25
|
+
state.visibility = "expanded";
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
expand: () => setVisibility("expanded"),
|
|
29
|
+
collapse: () => setVisibility("collapsed"),
|
|
30
|
+
close: () => setVisibility("closed"),
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { isWindowsPlatform } from "../../src/utils/platform";
|
|
3
|
+
import { registerDockCommand } from "./commands/dock";
|
|
4
|
+
import { registerPinCommand } from "./commands/pin";
|
|
5
|
+
import { type DockController, setupDockWidgets } from "./widget/setup";
|
|
6
|
+
|
|
7
|
+
export default async function processesDockExtension(
|
|
8
|
+
pi: ExtensionAPI,
|
|
9
|
+
): Promise<void> {
|
|
10
|
+
// The core extension no-ops on Windows; the dock has no backing manager to
|
|
11
|
+
// read from, so bail silently (the core extension already warned).
|
|
12
|
+
if (isWindowsPlatform()) return;
|
|
13
|
+
|
|
14
|
+
let controller: DockController | null = null;
|
|
15
|
+
|
|
16
|
+
registerDockCommand(pi, () => controller);
|
|
17
|
+
registerPinCommand(pi, pi.events, () => controller);
|
|
18
|
+
|
|
19
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
20
|
+
controller?.dispose();
|
|
21
|
+
controller = setupDockWidgets(ctx, pi.events);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
pi.on("session_shutdown", () => {
|
|
25
|
+
controller?.dispose();
|
|
26
|
+
controller = null;
|
|
27
|
+
});
|
|
28
|
+
}
|