@dimina-kit/devtools 0.4.0-dev.20260728065133 → 0.4.0-dev.20260728095034
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/main/app/app.js +11 -3
- package/dist/main/index.bundle.js +319 -81
- package/dist/main/ipc/settings.js +4 -0
- package/dist/main/services/mcp/server.d.ts +2 -1
- package/dist/main/services/mcp/server.js +6 -3
- package/dist/main/services/mcp/tools/project-tools.d.ts +49 -0
- package/dist/main/services/mcp/tools/project-tools.js +121 -0
- package/dist/main/services/notifications/renderer-notifier.d.ts +11 -0
- package/dist/main/services/notifications/renderer-notifier.js +3 -0
- package/dist/main/services/workbench-context.d.ts +14 -0
- package/dist/main/services/workbench-context.js +9 -1
- package/dist/main/services/workspace/compile-log-buffer.d.ts +40 -0
- package/dist/main/services/workspace/compile-log-buffer.js +41 -0
- package/dist/main/services/workspace/session-status-store.d.ts +47 -0
- package/dist/main/services/workspace/session-status-store.js +73 -0
- package/dist/main/services/workspace/status-tap.d.ts +20 -0
- package/dist/main/services/workspace/status-tap.js +32 -0
- package/dist/native-host/common/common.js +58 -154
- package/dist/native-host/container/pageFrame.css +1 -1
- package/dist/native-host/render/render.js +3297 -6602
- package/dist/native-host/service/service.js +2 -2
- package/dist/preload/windows/host-toolbar-runtime.cjs.map +2 -2
- package/dist/preload/windows/simulator.cjs.map +1 -1
- package/dist/renderer/assets/index-BJlFbXHb.js +49 -0
- package/dist/renderer/assets/{input-dsgeTmVX.js → input-xdmfv-eH.js} +2 -2
- package/dist/renderer/assets/{ipc-transport-BgFdT9bT.js → ipc-transport-B3EpmfNS.js} +2 -2
- package/dist/renderer/assets/popover-B88e2hkg.js +2 -0
- package/dist/renderer/assets/{select-DAw1hzkl.js → select-CD9CJyhI.js} +2 -2
- package/dist/renderer/assets/settings-44hf0QtO.js +2 -0
- package/dist/renderer/assets/settings-api-pJKPuP1M.js +2 -0
- package/dist/renderer/assets/workbenchSettings-DKD5U1Ql.js +8 -0
- package/dist/renderer/entries/main/index.html +5 -5
- package/dist/renderer/entries/popover/index.html +4 -4
- package/dist/renderer/entries/settings/index.html +4 -4
- package/dist/renderer/entries/workbench-settings/index.html +3 -3
- package/dist/shared/ipc-channels.d.ts +2 -0
- package/dist/shared/ipc-channels.js +2 -0
- package/package.json +6 -6
- package/dist/renderer/assets/index-M5NDq_vi.js +0 -49
- package/dist/renderer/assets/popover-Disfu1cx.js +0 -2
- package/dist/renderer/assets/settings-M_8GFW2M.js +0 -2
- package/dist/renderer/assets/settings-api-DFR5eDl_.js +0 -2
- package/dist/renderer/assets/workbenchSettings-CxdXeoOu.js +0 -8
|
@@ -20,6 +20,10 @@ export function registerSettingsIpc(ctx) {
|
|
|
20
20
|
.handle(WorkbenchSettingsChannel.SetTheme, (_, ...args) => {
|
|
21
21
|
const [theme] = validate(WorkbenchSettingsChannel.SetTheme, WorkbenchSettingsSetThemeSchema, args);
|
|
22
22
|
applyTheme(theme);
|
|
23
|
+
})
|
|
24
|
+
.handle(WorkbenchSettingsChannel.Restart, () => {
|
|
25
|
+
app.relaunch();
|
|
26
|
+
app.quit();
|
|
23
27
|
})
|
|
24
28
|
.handle(WorkbenchSettingsChannel.GetCdpStatus, () => {
|
|
25
29
|
const settings = loadWorkbenchSettings();
|
|
@@ -7,5 +7,6 @@
|
|
|
7
7
|
* only wires up transports and lifecycle.
|
|
8
8
|
*/
|
|
9
9
|
import { type Disposable } from '@dimina-kit/electron-deck/main';
|
|
10
|
-
|
|
10
|
+
import { type McpProjectHost } from './tools/project-tools.js';
|
|
11
|
+
export declare function startMcpServer(resolvedCdpPort: number, mcpPort: number, projectHost: McpProjectHost): Disposable;
|
|
11
12
|
//# sourceMappingURL=server.d.ts.map
|
|
@@ -15,11 +15,12 @@ import { connectTarget, setCdpPort } from './target-manager.js';
|
|
|
15
15
|
import { recordMcpFailed, recordMcpStarted, recordMcpStopped } from './status.js';
|
|
16
16
|
import { registerCommonTargetTools } from './tool-registry.js';
|
|
17
17
|
import { registerContextTools } from './tools/context-tools.js';
|
|
18
|
+
import { registerProjectTools } from './tools/project-tools.js';
|
|
18
19
|
import { registerSimulatorTools } from './tools/simulator-tools.js';
|
|
19
20
|
import { registerWorkbenchTools } from './tools/workbench-tools.js';
|
|
20
21
|
const require = createRequire(import.meta.url);
|
|
21
22
|
const pkg = require('../../../../package.json');
|
|
22
|
-
function buildServer() {
|
|
23
|
+
function buildServer(projectHost) {
|
|
23
24
|
const server = new McpServer({
|
|
24
25
|
name: '@dimina-kit/devtools',
|
|
25
26
|
version: pkg.version,
|
|
@@ -32,9 +33,11 @@ function buildServer() {
|
|
|
32
33
|
registerWorkbenchTools(server);
|
|
33
34
|
// Cross-target: AI orientation overview
|
|
34
35
|
registerContextTools(server);
|
|
36
|
+
// Project lifecycle (open/close/status/wait/compile logs).
|
|
37
|
+
registerProjectTools(server, projectHost);
|
|
35
38
|
return server;
|
|
36
39
|
}
|
|
37
|
-
export function startMcpServer(resolvedCdpPort, mcpPort) {
|
|
40
|
+
export function startMcpServer(resolvedCdpPort, mcpPort, projectHost) {
|
|
38
41
|
setCdpPort(resolvedCdpPort);
|
|
39
42
|
// Connect to both targets (non-blocking)
|
|
40
43
|
connectTarget('simulator').catch(() => { });
|
|
@@ -55,7 +58,7 @@ export function startMcpServer(resolvedCdpPort, mcpPort) {
|
|
|
55
58
|
// closures over already-shared module-level state (target-manager's
|
|
56
59
|
// CDP connections/listeners) — it has no per-call side effects, so
|
|
57
60
|
// building N servers is cheap and duplicates nothing.
|
|
58
|
-
await buildServer().connect(transport);
|
|
61
|
+
await buildServer(projectHost).connect(transport);
|
|
59
62
|
}
|
|
60
63
|
catch (err) {
|
|
61
64
|
transports.delete(transport.sessionId);
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP project lifecycle tools: open (launch the mini-program in the
|
|
3
|
+
* simulator), close, query compile status, await the compile settling, and
|
|
4
|
+
* pull compile logs incrementally.
|
|
5
|
+
*
|
|
6
|
+
* `project_open` does NOT call `workspace.openProject` directly: the renderer
|
|
7
|
+
* owns the open path (mounting ProjectRuntime is what compiles AND attaches
|
|
8
|
+
* the simulator), so the tool pushes a `window:openProject` event and then
|
|
9
|
+
* awaits the session-status store — the same main-process authority the
|
|
10
|
+
* notifier tap feeds from the renderer-driven compile.
|
|
11
|
+
*/
|
|
12
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
13
|
+
import type { CompileLogBuffer } from '../../workspace/compile-log-buffer.js';
|
|
14
|
+
import type { SessionStatusStore } from '../../workspace/session-status-store.js';
|
|
15
|
+
/**
|
|
16
|
+
* Narrow structural slice of WorkspaceService the project tools need.
|
|
17
|
+
* Structural (not a Pick of the real interface) so tests can hand in a
|
|
18
|
+
* plain object and this module never imports workbench-context.
|
|
19
|
+
*/
|
|
20
|
+
export interface McpProjectWorkspace {
|
|
21
|
+
validateProjectDir(dirPath: string): Promise<string | null>;
|
|
22
|
+
hasProject(dirPath: string): Promise<boolean>;
|
|
23
|
+
addProject(dirPath: string): Promise<{
|
|
24
|
+
name: string;
|
|
25
|
+
path: string;
|
|
26
|
+
}>;
|
|
27
|
+
listProjects(): Promise<{
|
|
28
|
+
name: string;
|
|
29
|
+
path: string;
|
|
30
|
+
}[]>;
|
|
31
|
+
closeProject(): Promise<void>;
|
|
32
|
+
getProjectPath(): string;
|
|
33
|
+
hasActiveSession(): boolean;
|
|
34
|
+
}
|
|
35
|
+
/** Everything `registerProjectTools` needs, assembled by app bootstrap. */
|
|
36
|
+
export interface McpProjectHost {
|
|
37
|
+
workspace: McpProjectWorkspace;
|
|
38
|
+
sessionStatus: SessionStatusStore;
|
|
39
|
+
compileLogs: CompileLogBuffer;
|
|
40
|
+
/** Push the renderer to open the project (the user-click-equivalent path). */
|
|
41
|
+
requestOpenInUi(project: {
|
|
42
|
+
name: string;
|
|
43
|
+
path: string;
|
|
44
|
+
}): void;
|
|
45
|
+
/** Push the renderer back to the project list after a close. */
|
|
46
|
+
requestNavigateBack(): void;
|
|
47
|
+
}
|
|
48
|
+
export declare function registerProjectTools(server: McpServer, host: McpProjectHost): void;
|
|
49
|
+
//# sourceMappingURL=project-tools.d.ts.map
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP project lifecycle tools: open (launch the mini-program in the
|
|
3
|
+
* simulator), close, query compile status, await the compile settling, and
|
|
4
|
+
* pull compile logs incrementally.
|
|
5
|
+
*
|
|
6
|
+
* `project_open` does NOT call `workspace.openProject` directly: the renderer
|
|
7
|
+
* owns the open path (mounting ProjectRuntime is what compiles AND attaches
|
|
8
|
+
* the simulator), so the tool pushes a `window:openProject` event and then
|
|
9
|
+
* awaits the session-status store — the same main-process authority the
|
|
10
|
+
* notifier tap feeds from the renderer-driven compile.
|
|
11
|
+
*/
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
const DEFAULT_OPEN_TIMEOUT_MS = 180_000;
|
|
15
|
+
const DEFAULT_WAIT_TIMEOUT_MS = 60_000;
|
|
16
|
+
function text(payload) {
|
|
17
|
+
return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
|
|
18
|
+
}
|
|
19
|
+
function errorText(message) {
|
|
20
|
+
return { content: [{ type: 'text', text: `Error: ${message}` }], isError: true };
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Public phase: a settled store snapshot with no live session means no
|
|
24
|
+
* project is open ('idle'), while 'error'/'compiling' pass through as-is —
|
|
25
|
+
* `hasActiveSession()` is false during a compile and after a failed open,
|
|
26
|
+
* so those phases are the truthful answer even without a session.
|
|
27
|
+
*/
|
|
28
|
+
function publicPhase(snapshot, sessionActive) {
|
|
29
|
+
if (!sessionActive && snapshot.phase === 'ready')
|
|
30
|
+
return 'idle';
|
|
31
|
+
return snapshot.phase;
|
|
32
|
+
}
|
|
33
|
+
function statusPayload(host) {
|
|
34
|
+
const snapshot = host.sessionStatus.get();
|
|
35
|
+
const sessionActive = host.workspace.hasActiveSession();
|
|
36
|
+
return {
|
|
37
|
+
phase: publicPhase(snapshot, sessionActive),
|
|
38
|
+
message: snapshot.message,
|
|
39
|
+
projectPath: host.workspace.getProjectPath(),
|
|
40
|
+
sessionActive,
|
|
41
|
+
watcherAlive: snapshot.watcherAlive,
|
|
42
|
+
updatedAt: snapshot.updatedAt,
|
|
43
|
+
generation: snapshot.generation,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export function registerProjectTools(server, host) {
|
|
47
|
+
server.tool('project_open', 'Open a mini-program project: registers the directory if needed, launches it in the simulator (compile + attach), and waits until the compile settles. Returns the settled status; on compile error, pull stderr via compile_logs.', {
|
|
48
|
+
path: z.string().describe('Absolute path to the mini-program project directory'),
|
|
49
|
+
timeoutMs: z.number().optional().default(DEFAULT_OPEN_TIMEOUT_MS)
|
|
50
|
+
.describe('Max time to wait for the compile to settle'),
|
|
51
|
+
}, async ({ path: rawPath, timeoutMs }) => {
|
|
52
|
+
const dir = path.resolve(rawPath);
|
|
53
|
+
// Already open and settled: report instead of re-triggering a compile.
|
|
54
|
+
const current = host.sessionStatus.get();
|
|
55
|
+
if (host.workspace.getProjectPath() === dir
|
|
56
|
+
&& host.workspace.hasActiveSession()
|
|
57
|
+
&& current.phase === 'ready') {
|
|
58
|
+
return text({ ...statusPayload(host), note: 'project already open' });
|
|
59
|
+
}
|
|
60
|
+
const invalid = await host.workspace.validateProjectDir(dir);
|
|
61
|
+
if (invalid)
|
|
62
|
+
return errorText(invalid);
|
|
63
|
+
let project;
|
|
64
|
+
if (await host.workspace.hasProject(dir)) {
|
|
65
|
+
const known = (await host.workspace.listProjects()).find((p) => p.path === dir);
|
|
66
|
+
project = known ?? { name: path.basename(dir), path: dir };
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
project = await host.workspace.addProject(dir);
|
|
70
|
+
}
|
|
71
|
+
// Snapshot the generation BEFORE the trigger so a previous session's
|
|
72
|
+
// settled state can never be mistaken for this open's result.
|
|
73
|
+
const afterGeneration = host.sessionStatus.get().generation;
|
|
74
|
+
host.requestOpenInUi({ name: project.name, path: project.path });
|
|
75
|
+
let settled;
|
|
76
|
+
try {
|
|
77
|
+
settled = await host.sessionStatus.waitForSettled({ afterGeneration, timeoutMs });
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
return errorText(err instanceof Error ? err.message : String(err));
|
|
81
|
+
}
|
|
82
|
+
if (settled.phase === 'error') {
|
|
83
|
+
return errorText(`compile failed: ${settled.message} — call compile_logs (stream: "stderr") for details`);
|
|
84
|
+
}
|
|
85
|
+
return text(statusPayload(host));
|
|
86
|
+
});
|
|
87
|
+
server.tool('project_close', 'Close the currently open project session and return the workbench to the project list.', {}, async () => {
|
|
88
|
+
if (!host.workspace.getProjectPath() && !host.workspace.hasActiveSession()) {
|
|
89
|
+
return text({ closed: false, note: 'no project is open' });
|
|
90
|
+
}
|
|
91
|
+
await host.workspace.closeProject();
|
|
92
|
+
host.requestNavigateBack();
|
|
93
|
+
return text({ closed: true });
|
|
94
|
+
});
|
|
95
|
+
server.tool('project_status', 'Query the current project compile status (phase: idle | compiling | ready | error). Includes `generation` — pass it to project_wait_ready to await the NEXT settled state.', {}, async () => text(statusPayload(host)));
|
|
96
|
+
server.tool('project_wait_ready', 'Wait until the compile pipeline settles (phase leaves "compiling"). Watcher rebuilds emit no "compiling" phase, so to await a rebuild after editing files pass the `generation` from a prior project_status.', {
|
|
97
|
+
afterGeneration: z.number().optional()
|
|
98
|
+
.describe('Only accept states recorded after this generation (from project_status). Omit to accept the current state when already settled.'),
|
|
99
|
+
timeoutMs: z.number().optional().default(DEFAULT_WAIT_TIMEOUT_MS)
|
|
100
|
+
.describe('Max time to wait'),
|
|
101
|
+
}, async ({ afterGeneration, timeoutMs }) => {
|
|
102
|
+
try {
|
|
103
|
+
await host.sessionStatus.waitForSettled({ afterGeneration, timeoutMs });
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
return errorText(err instanceof Error ? err.message : String(err));
|
|
107
|
+
}
|
|
108
|
+
return text(statusPayload(host));
|
|
109
|
+
});
|
|
110
|
+
server.tool('compile_logs', 'Pull compile log lines incrementally. Pass the previous call\'s nextCursor as `cursor` to receive only new lines. The buffer resets on each fresh project open; watcher rebuilds append to the same timeline.', {
|
|
111
|
+
cursor: z.number().optional().default(0)
|
|
112
|
+
.describe('Return only lines after this cursor (0 = from the start of the buffer)'),
|
|
113
|
+
limit: z.number().optional().default(100).describe('Max lines to return'),
|
|
114
|
+
stream: z.enum(['stdout', 'stderr']).optional()
|
|
115
|
+
.describe('Only lines from this stream (stderr carries compile errors)'),
|
|
116
|
+
}, async ({ cursor, limit, stream }) => {
|
|
117
|
+
const read = host.compileLogs.read({ afterSeq: cursor, limit, stream });
|
|
118
|
+
return text(read);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=project-tools.js.map
|
|
@@ -94,6 +94,17 @@ export interface RendererNotifier {
|
|
|
94
94
|
compileLog(payload: CompileLogPayload): void;
|
|
95
95
|
/** Ask the main renderer to navigate back to its landing screen. */
|
|
96
96
|
windowNavigateBack(): void;
|
|
97
|
+
/**
|
|
98
|
+
* Ask the main renderer to open a project (mount the project runtime and
|
|
99
|
+
* start the compile), exactly as if the user clicked it in the list. The
|
|
100
|
+
* renderer owns the open path — the simulator only mounts when the
|
|
101
|
+
* ProjectRuntime component does — so main-side callers (MCP `project_open`)
|
|
102
|
+
* push this instead of calling `workspace.openProject` directly.
|
|
103
|
+
*/
|
|
104
|
+
windowOpenProject(payload: {
|
|
105
|
+
name: string;
|
|
106
|
+
path: string;
|
|
107
|
+
}): void;
|
|
97
108
|
/** Tell the main renderer the compile popover has been closed. */
|
|
98
109
|
popoverClosed(): void;
|
|
99
110
|
/** Ask the main renderer to relaunch the simulator with a new config. */
|
|
@@ -33,6 +33,9 @@ export function createRendererNotifier(ctx) {
|
|
|
33
33
|
windowNavigateBack() {
|
|
34
34
|
sendToMain(WindowChannel.NavigateBack);
|
|
35
35
|
},
|
|
36
|
+
windowOpenProject(payload) {
|
|
37
|
+
sendToMain(WindowChannel.OpenProject, payload);
|
|
38
|
+
},
|
|
36
39
|
popoverClosed() {
|
|
37
40
|
sendToMain(PopoverChannel.Closed);
|
|
38
41
|
},
|
|
@@ -12,6 +12,8 @@ import type { SyncStorageChange } from '../../shared/ipc-channels.js';
|
|
|
12
12
|
import { DisposableRegistry, type ConnectionRegistry } from '@dimina-kit/electron-deck/main';
|
|
13
13
|
import type { SenderPolicy } from '../utils/ipc-registry.js';
|
|
14
14
|
import { type RendererNotifier } from './notifications/renderer-notifier.js';
|
|
15
|
+
import { type CompileLogBuffer } from './workspace/compile-log-buffer.js';
|
|
16
|
+
import { type SessionStatusStore } from './workspace/session-status-store.js';
|
|
15
17
|
import { type ViewManager } from './views/view-manager.js';
|
|
16
18
|
import { type WindowService } from './window-service.js';
|
|
17
19
|
import { type WorkspaceService } from './workspace/workspace-service.js';
|
|
@@ -47,6 +49,18 @@ export interface WorkbenchContext {
|
|
|
47
49
|
windows: WindowService;
|
|
48
50
|
/** Unified main → renderer event dispatcher */
|
|
49
51
|
notify: RendererNotifier;
|
|
52
|
+
/**
|
|
53
|
+
* Authoritative, queryable compile status of the active session. Fed by the
|
|
54
|
+
* notifier tap (every `notify.projectStatus` is recorded here first), read
|
|
55
|
+
* by MCP `project_status` / awaited by `project_open` / `project_wait_ready`.
|
|
56
|
+
*/
|
|
57
|
+
sessionStatus: SessionStatusStore;
|
|
58
|
+
/**
|
|
59
|
+
* Cursor-readable ring buffer over the compile log lines. Fed by the
|
|
60
|
+
* notifier tap (every `notify.compileLog` is appended here first), read
|
|
61
|
+
* incrementally by MCP `compile_logs`.
|
|
62
|
+
*/
|
|
63
|
+
compileLogBuffer: CompileLogBuffer;
|
|
50
64
|
/**
|
|
51
65
|
* Open (or re-focus) the standalone workbench-settings window. First-class
|
|
52
66
|
* member so contract holders (`MiniappRuntime` / `MenuContext`) can open
|
|
@@ -3,6 +3,9 @@ import { createConnectionRegistry, DisposableRegistry, } from '@dimina-kit/elect
|
|
|
3
3
|
import { createWorkbenchSenderPolicy } from '../utils/sender-policy.js';
|
|
4
4
|
import { defaultAdapter } from './default-adapter.js';
|
|
5
5
|
import { createRendererNotifier, } from './notifications/renderer-notifier.js';
|
|
6
|
+
import { createCompileLogBuffer, } from './workspace/compile-log-buffer.js';
|
|
7
|
+
import { createSessionStatusStore, } from './workspace/session-status-store.js';
|
|
8
|
+
import { tapNotifierIntoStores } from './workspace/status-tap.js';
|
|
6
9
|
import { createViewManager } from './views/view-manager.js';
|
|
7
10
|
import { createWindowService } from './window-service.js';
|
|
8
11
|
import { openSettingsWindow } from '../windows/settings-window/index.js';
|
|
@@ -38,7 +41,12 @@ export function createWorkbenchContext(opts) {
|
|
|
38
41
|
// the one place that releases the HOST-scoped toolbar (its view and the
|
|
39
42
|
// ref-counted session-runtime preload) when the app/context winds down.
|
|
40
43
|
ctx.registry.add(() => ctx.views.disposeAll());
|
|
41
|
-
ctx.
|
|
44
|
+
ctx.sessionStatus = createSessionStatusStore();
|
|
45
|
+
ctx.compileLogBuffer = createCompileLogBuffer();
|
|
46
|
+
// Tap the notify gate: the two compile-state pushes land in the
|
|
47
|
+
// main-process authorities above BEFORE the renderer broadcast, so MCP
|
|
48
|
+
// reads and the renderer view derive from the same single site.
|
|
49
|
+
ctx.notify = tapNotifierIntoStores(createRendererNotifier(ctx), ctx.sessionStatus, ctx.compileLogBuffer);
|
|
42
50
|
// Lazy closure (not a bound snapshot): reads ctx.windows/notify/rendererDir
|
|
43
51
|
// at call time through the live context, which structurally satisfies the
|
|
44
52
|
// helper's narrow OpenSettingsWindowDeps.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ring buffer over the per-line dmcc compile log, readable by cursor.
|
|
3
|
+
*
|
|
4
|
+
* The renderer gets each line pushed (`notify.compileLog`); MCP clients pull
|
|
5
|
+
* incrementally instead: pass the previous read's `nextCursor` to receive
|
|
6
|
+
* only lines that arrived since. `seq` is monotonic for the buffer's whole
|
|
7
|
+
* lifetime — `clear()` (a fresh project open) drops the entries but never
|
|
8
|
+
* resets the counter, so a stale cursor from before the clear simply reads
|
|
9
|
+
* the new compile's lines instead of duplicating old ones.
|
|
10
|
+
*/
|
|
11
|
+
export interface CompileLogRecord {
|
|
12
|
+
/** Monotonic id (1-based); never reused, survives clear(). */
|
|
13
|
+
seq: number;
|
|
14
|
+
/** Main-process capture time of the line. */
|
|
15
|
+
at: number;
|
|
16
|
+
stream: 'stdout' | 'stderr';
|
|
17
|
+
text: string;
|
|
18
|
+
}
|
|
19
|
+
export interface CompileLogRead {
|
|
20
|
+
/** Oldest-first entries with seq > afterSeq (post stream-filter), capped at limit. */
|
|
21
|
+
entries: CompileLogRecord[];
|
|
22
|
+
/** Pass back as the next read's afterSeq; unchanged when nothing new matched. */
|
|
23
|
+
nextCursor: number;
|
|
24
|
+
}
|
|
25
|
+
export interface CompileLogBuffer {
|
|
26
|
+
append(entry: {
|
|
27
|
+
at: number;
|
|
28
|
+
stream: 'stdout' | 'stderr';
|
|
29
|
+
text: string;
|
|
30
|
+
}): void;
|
|
31
|
+
read(opts?: {
|
|
32
|
+
afterSeq?: number;
|
|
33
|
+
limit?: number;
|
|
34
|
+
stream?: 'stdout' | 'stderr';
|
|
35
|
+
}): CompileLogRead;
|
|
36
|
+
/** Drop all entries (fresh compile timeline); seq keeps growing. */
|
|
37
|
+
clear(): void;
|
|
38
|
+
}
|
|
39
|
+
export declare function createCompileLogBuffer(capacity?: number): CompileLogBuffer;
|
|
40
|
+
//# sourceMappingURL=compile-log-buffer.d.ts.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ring buffer over the per-line dmcc compile log, readable by cursor.
|
|
3
|
+
*
|
|
4
|
+
* The renderer gets each line pushed (`notify.compileLog`); MCP clients pull
|
|
5
|
+
* incrementally instead: pass the previous read's `nextCursor` to receive
|
|
6
|
+
* only lines that arrived since. `seq` is monotonic for the buffer's whole
|
|
7
|
+
* lifetime — `clear()` (a fresh project open) drops the entries but never
|
|
8
|
+
* resets the counter, so a stale cursor from before the clear simply reads
|
|
9
|
+
* the new compile's lines instead of duplicating old ones.
|
|
10
|
+
*/
|
|
11
|
+
const DEFAULT_CAPACITY = 1000;
|
|
12
|
+
const DEFAULT_READ_LIMIT = 100;
|
|
13
|
+
export function createCompileLogBuffer(capacity = DEFAULT_CAPACITY) {
|
|
14
|
+
const entries = [];
|
|
15
|
+
let seq = 0;
|
|
16
|
+
return {
|
|
17
|
+
append(entry) {
|
|
18
|
+
entries.push({ seq: ++seq, ...entry });
|
|
19
|
+
if (entries.length > capacity)
|
|
20
|
+
entries.splice(0, entries.length - capacity);
|
|
21
|
+
},
|
|
22
|
+
read({ afterSeq = 0, limit = DEFAULT_READ_LIMIT, stream } = {}) {
|
|
23
|
+
const matched = [];
|
|
24
|
+
for (const entry of entries) {
|
|
25
|
+
if (entry.seq <= afterSeq)
|
|
26
|
+
continue;
|
|
27
|
+
if (stream && entry.stream !== stream)
|
|
28
|
+
continue;
|
|
29
|
+
matched.push(entry);
|
|
30
|
+
if (matched.length >= limit)
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
const last = matched[matched.length - 1];
|
|
34
|
+
return { entries: matched, nextCursor: last ? last.seq : afterSeq };
|
|
35
|
+
},
|
|
36
|
+
clear() {
|
|
37
|
+
entries.length = 0;
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=compile-log-buffer.js.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authoritative, queryable record of the active project's compile status in
|
|
3
|
+
* the main process.
|
|
4
|
+
*
|
|
5
|
+
* `notify.projectStatus` is a fire-and-forget push to the renderer; MCP
|
|
6
|
+
* clients instead need to QUERY the latest state and AWAIT the next settled
|
|
7
|
+
* state. The notifier tap (see `status-tap.ts`) records every payload here
|
|
8
|
+
* BEFORE it is broadcast — the store is the authority, the renderer push is
|
|
9
|
+
* derived from the same single gate.
|
|
10
|
+
*
|
|
11
|
+
* "Settled" means the compile pipeline is not mid-flight: every phase except
|
|
12
|
+
* `'compiling'`. `generation` is a monotonic per-record counter so a waiter
|
|
13
|
+
* can ignore states recorded before its own trigger (e.g. `project_open`
|
|
14
|
+
* must not accept the PREVIOUS session's `'ready'` as its own result).
|
|
15
|
+
*/
|
|
16
|
+
export type SessionPhase = 'idle' | 'compiling' | 'ready' | 'error';
|
|
17
|
+
export interface SessionStatusSnapshot {
|
|
18
|
+
phase: SessionPhase;
|
|
19
|
+
message: string;
|
|
20
|
+
/** False once the file watcher died mid-session (saves no longer trigger a rebuild). Reset when the next compile starts. */
|
|
21
|
+
watcherAlive: boolean;
|
|
22
|
+
/** Wall-clock time of the last recorded transition; 0 before the first one. */
|
|
23
|
+
updatedAt: number;
|
|
24
|
+
/** Monotonic counter bumped on every record. */
|
|
25
|
+
generation: number;
|
|
26
|
+
}
|
|
27
|
+
export interface SessionStatusStore {
|
|
28
|
+
get(): SessionStatusSnapshot;
|
|
29
|
+
/** Record one `projectStatus` payload (the notifier tap is the only expected caller). */
|
|
30
|
+
record(payload: {
|
|
31
|
+
status: string;
|
|
32
|
+
message: string;
|
|
33
|
+
watcher?: 'dead';
|
|
34
|
+
}): void;
|
|
35
|
+
/**
|
|
36
|
+
* Resolve with the first snapshot whose phase is settled (not 'compiling')
|
|
37
|
+
* AND whose generation is greater than `afterGeneration` (default: accept
|
|
38
|
+
* the current snapshot when it is already settled). Rejects after
|
|
39
|
+
* `timeoutMs` with the last observed phase in the message.
|
|
40
|
+
*/
|
|
41
|
+
waitForSettled(opts: {
|
|
42
|
+
afterGeneration?: number;
|
|
43
|
+
timeoutMs: number;
|
|
44
|
+
}): Promise<SessionStatusSnapshot>;
|
|
45
|
+
}
|
|
46
|
+
export declare function createSessionStatusStore(): SessionStatusStore;
|
|
47
|
+
//# sourceMappingURL=session-status-store.d.ts.map
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authoritative, queryable record of the active project's compile status in
|
|
3
|
+
* the main process.
|
|
4
|
+
*
|
|
5
|
+
* `notify.projectStatus` is a fire-and-forget push to the renderer; MCP
|
|
6
|
+
* clients instead need to QUERY the latest state and AWAIT the next settled
|
|
7
|
+
* state. The notifier tap (see `status-tap.ts`) records every payload here
|
|
8
|
+
* BEFORE it is broadcast — the store is the authority, the renderer push is
|
|
9
|
+
* derived from the same single gate.
|
|
10
|
+
*
|
|
11
|
+
* "Settled" means the compile pipeline is not mid-flight: every phase except
|
|
12
|
+
* `'compiling'`. `generation` is a monotonic per-record counter so a waiter
|
|
13
|
+
* can ignore states recorded before its own trigger (e.g. `project_open`
|
|
14
|
+
* must not accept the PREVIOUS session's `'ready'` as its own result).
|
|
15
|
+
*/
|
|
16
|
+
function toPhase(status) {
|
|
17
|
+
if (status === 'compiling')
|
|
18
|
+
return 'compiling';
|
|
19
|
+
if (status === 'error')
|
|
20
|
+
return 'error';
|
|
21
|
+
// 'ready' plus any future non-error chatter: the pipeline is not mid-flight.
|
|
22
|
+
return 'ready';
|
|
23
|
+
}
|
|
24
|
+
export function createSessionStatusStore() {
|
|
25
|
+
let snapshot = {
|
|
26
|
+
phase: 'idle',
|
|
27
|
+
message: '',
|
|
28
|
+
watcherAlive: true,
|
|
29
|
+
updatedAt: 0,
|
|
30
|
+
generation: 0,
|
|
31
|
+
};
|
|
32
|
+
const waiters = new Set();
|
|
33
|
+
return {
|
|
34
|
+
get: () => snapshot,
|
|
35
|
+
record(payload) {
|
|
36
|
+
const phase = toPhase(payload.status);
|
|
37
|
+
snapshot = {
|
|
38
|
+
phase,
|
|
39
|
+
message: payload.message,
|
|
40
|
+
// A dead watcher is a session-lifetime fact; only a NEW compile
|
|
41
|
+
// (fresh open) resets it. Rebuild 'ready' chatter keeps it dead.
|
|
42
|
+
watcherAlive: payload.watcher === 'dead'
|
|
43
|
+
? false
|
|
44
|
+
: phase === 'compiling' ? true : snapshot.watcherAlive,
|
|
45
|
+
updatedAt: Date.now(),
|
|
46
|
+
generation: snapshot.generation + 1,
|
|
47
|
+
};
|
|
48
|
+
for (const waiter of [...waiters])
|
|
49
|
+
waiter(snapshot);
|
|
50
|
+
},
|
|
51
|
+
waitForSettled({ afterGeneration = -1, timeoutMs }) {
|
|
52
|
+
const isSettled = (s) => s.phase !== 'compiling' && s.generation > afterGeneration;
|
|
53
|
+
if (isSettled(snapshot))
|
|
54
|
+
return Promise.resolve(snapshot);
|
|
55
|
+
return new Promise((resolve, reject) => {
|
|
56
|
+
const timer = setTimeout(() => {
|
|
57
|
+
waiters.delete(onRecord);
|
|
58
|
+
reject(new Error(`timed out after ${timeoutMs}ms waiting for the compile to settle `
|
|
59
|
+
+ `(last phase: ${snapshot.phase})`));
|
|
60
|
+
}, timeoutMs);
|
|
61
|
+
const onRecord = (s) => {
|
|
62
|
+
if (!isSettled(s))
|
|
63
|
+
return;
|
|
64
|
+
clearTimeout(timer);
|
|
65
|
+
waiters.delete(onRecord);
|
|
66
|
+
resolve(s);
|
|
67
|
+
};
|
|
68
|
+
waiters.add(onRecord);
|
|
69
|
+
});
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=session-status-store.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { RendererNotifier } from '../notifications/renderer-notifier.js';
|
|
2
|
+
import type { CompileLogBuffer } from './compile-log-buffer.js';
|
|
3
|
+
import type { SessionStatusStore } from './session-status-store.js';
|
|
4
|
+
/**
|
|
5
|
+
* Wraps the renderer notifier so the two compile-state pushes ALSO land in
|
|
6
|
+
* the main-process authorities (session-status store + compile-log buffer)
|
|
7
|
+
* before the renderer broadcast.
|
|
8
|
+
*
|
|
9
|
+
* `ctx.notify` is the single outlet every status transition and log line
|
|
10
|
+
* already flows through (`workspace-service`'s `sendStatus` / `onLog`
|
|
11
|
+
* closures), so tapping at this gate keeps ONE bookkeeping site instead of a
|
|
12
|
+
* second call next to every producer — and the stale-session generation
|
|
13
|
+
* guard upstream of the notify call protects the store/buffer for free.
|
|
14
|
+
*
|
|
15
|
+
* A fresh open (`'compiling'` without `hotReload`) starts a new compile
|
|
16
|
+
* timeline, so it clears the buffer; watcher rebuilds never pass through
|
|
17
|
+
* `'compiling'` and keep appending to the same timeline.
|
|
18
|
+
*/
|
|
19
|
+
export declare function tapNotifierIntoStores(notify: RendererNotifier, status: SessionStatusStore, compileLogs: CompileLogBuffer): RendererNotifier;
|
|
20
|
+
//# sourceMappingURL=status-tap.d.ts.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wraps the renderer notifier so the two compile-state pushes ALSO land in
|
|
3
|
+
* the main-process authorities (session-status store + compile-log buffer)
|
|
4
|
+
* before the renderer broadcast.
|
|
5
|
+
*
|
|
6
|
+
* `ctx.notify` is the single outlet every status transition and log line
|
|
7
|
+
* already flows through (`workspace-service`'s `sendStatus` / `onLog`
|
|
8
|
+
* closures), so tapping at this gate keeps ONE bookkeeping site instead of a
|
|
9
|
+
* second call next to every producer — and the stale-session generation
|
|
10
|
+
* guard upstream of the notify call protects the store/buffer for free.
|
|
11
|
+
*
|
|
12
|
+
* A fresh open (`'compiling'` without `hotReload`) starts a new compile
|
|
13
|
+
* timeline, so it clears the buffer; watcher rebuilds never pass through
|
|
14
|
+
* `'compiling'` and keep appending to the same timeline.
|
|
15
|
+
*/
|
|
16
|
+
export function tapNotifierIntoStores(notify, status, compileLogs) {
|
|
17
|
+
return {
|
|
18
|
+
...notify,
|
|
19
|
+
projectStatus(payload) {
|
|
20
|
+
if (payload.status === 'compiling' && payload.hotReload !== true) {
|
|
21
|
+
compileLogs.clear();
|
|
22
|
+
}
|
|
23
|
+
status.record(payload);
|
|
24
|
+
notify.projectStatus(payload);
|
|
25
|
+
},
|
|
26
|
+
compileLog(payload) {
|
|
27
|
+
compileLogs.append(payload);
|
|
28
|
+
notify.compileLog(payload);
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=status-tap.js.map
|