@pooder/core 2.0.0 → 2.2.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/CHANGELOG.md +12 -0
- package/dist/index.d.mts +196 -21
- package/dist/index.d.ts +196 -21
- package/dist/index.js +718 -157
- package/dist/index.mjs +707 -156
- package/package.json +1 -1
- package/src/command.ts +10 -10
- package/src/context.ts +22 -17
- package/src/contribution/index.ts +12 -12
- package/src/contribution/points.ts +27 -3
- package/src/contribution/registry.ts +118 -118
- package/src/disposable.ts +3 -3
- package/src/extension.ts +177 -164
- package/src/index.ts +338 -145
- package/src/run-test-full.ts +98 -98
- package/src/service.ts +191 -11
- package/src/services/CommandService.ts +79 -79
- package/src/services/ConfigurationService.ts +107 -107
- package/src/services/ToolRegistryService.ts +41 -0
- package/src/services/ToolSessionService.ts +213 -0
- package/src/services/WorkbenchService.ts +187 -8
- package/src/services/index.ts +23 -1
- package/src/services/tokens.ts +27 -0
- package/src/test-extension-full.ts +79 -79
|
@@ -1,107 +1,107 @@
|
|
|
1
|
-
import { Service } from "../service";
|
|
2
|
-
import EventBus from "../event";
|
|
3
|
-
import { ConfigurationContribution } from "../contribution";
|
|
4
|
-
|
|
5
|
-
export default class ConfigurationService implements Service {
|
|
6
|
-
private configValues: Map<string, any> = new Map();
|
|
7
|
-
private eventBus: EventBus = new EventBus();
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Get a configuration value.
|
|
11
|
-
*/
|
|
12
|
-
get<T = any>(key: string, defaultValue?: T): T {
|
|
13
|
-
if (this.configValues.has(key)) {
|
|
14
|
-
return this.configValues.get(key);
|
|
15
|
-
}
|
|
16
|
-
return defaultValue as T;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Update a configuration value.
|
|
21
|
-
* Emits 'change' event.
|
|
22
|
-
*/
|
|
23
|
-
update(key: string, value: any) {
|
|
24
|
-
const oldValue = this.configValues.get(key);
|
|
25
|
-
if (oldValue !== value) {
|
|
26
|
-
this.configValues.set(key, value);
|
|
27
|
-
this.eventBus.emit(`change:${key}`, { key, value, oldValue });
|
|
28
|
-
this.eventBus.emit("change", { key, value, oldValue });
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Listen for changes to a specific configuration key.
|
|
34
|
-
*/
|
|
35
|
-
onDidChange(
|
|
36
|
-
key: string,
|
|
37
|
-
callback: (event: { key: string; value: any; oldValue: any }) => void,
|
|
38
|
-
) {
|
|
39
|
-
this.eventBus.on(`change:${key}`, callback);
|
|
40
|
-
return {
|
|
41
|
-
dispose: () => this.eventBus.off(`change:${key}`, callback),
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Listen for any configuration change.
|
|
47
|
-
*/
|
|
48
|
-
onAnyChange(
|
|
49
|
-
callback: (event: { key: string; value: any; oldValue: any }) => void,
|
|
50
|
-
) {
|
|
51
|
-
this.eventBus.on("change", callback);
|
|
52
|
-
return {
|
|
53
|
-
dispose: () => this.eventBus.off("change", callback),
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Export current configuration state as a JSON-serializable object.
|
|
59
|
-
* Useful for saving configuration templates.
|
|
60
|
-
*/
|
|
61
|
-
export(): Record<string, any> {
|
|
62
|
-
const exportData: Record<string, any> = {};
|
|
63
|
-
for (const [key, value] of this.configValues) {
|
|
64
|
-
exportData[key] = value;
|
|
65
|
-
}
|
|
66
|
-
return exportData;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Import configuration from a JSON object.
|
|
71
|
-
* This will merge the provided configuration with the current state,
|
|
72
|
-
* overwriting existing keys and triggering change events.
|
|
73
|
-
*/
|
|
74
|
-
import(data: Record<string, any>): void {
|
|
75
|
-
if (!data || typeof data !== "object") {
|
|
76
|
-
console.warn("ConfigurationService: Import data must be an object.");
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
Object.entries(data).forEach(([key, value]) => {
|
|
80
|
-
this.update(key, value);
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Initialize configuration with defaults from contributions.
|
|
86
|
-
* This should be called when a contribution is registered.
|
|
87
|
-
*/
|
|
88
|
-
initializeDefaults(contributions: ConfigurationContribution[]) {
|
|
89
|
-
contributions.forEach((contrib) => {
|
|
90
|
-
if (!contrib.id) {
|
|
91
|
-
console.warn(
|
|
92
|
-
"Configuration contribution missing 'id'. Skipping default initialization.",
|
|
93
|
-
contrib,
|
|
94
|
-
);
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
if (!this.configValues.has(contrib.id) && contrib.default !== undefined) {
|
|
98
|
-
this.configValues.set(contrib.id, contrib.default);
|
|
99
|
-
}
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
dispose() {
|
|
104
|
-
this.configValues.clear();
|
|
105
|
-
// EventBus doesn't have a clear/dispose in the snippet, but it's fine for now.
|
|
106
|
-
}
|
|
107
|
-
}
|
|
1
|
+
import { Service } from "../service";
|
|
2
|
+
import EventBus from "../event";
|
|
3
|
+
import { ConfigurationContribution } from "../contribution";
|
|
4
|
+
|
|
5
|
+
export default class ConfigurationService implements Service {
|
|
6
|
+
private configValues: Map<string, any> = new Map();
|
|
7
|
+
private eventBus: EventBus = new EventBus();
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Get a configuration value.
|
|
11
|
+
*/
|
|
12
|
+
get<T = any>(key: string, defaultValue?: T): T {
|
|
13
|
+
if (this.configValues.has(key)) {
|
|
14
|
+
return this.configValues.get(key);
|
|
15
|
+
}
|
|
16
|
+
return defaultValue as T;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Update a configuration value.
|
|
21
|
+
* Emits 'change' event.
|
|
22
|
+
*/
|
|
23
|
+
update(key: string, value: any) {
|
|
24
|
+
const oldValue = this.configValues.get(key);
|
|
25
|
+
if (oldValue !== value) {
|
|
26
|
+
this.configValues.set(key, value);
|
|
27
|
+
this.eventBus.emit(`change:${key}`, { key, value, oldValue });
|
|
28
|
+
this.eventBus.emit("change", { key, value, oldValue });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Listen for changes to a specific configuration key.
|
|
34
|
+
*/
|
|
35
|
+
onDidChange(
|
|
36
|
+
key: string,
|
|
37
|
+
callback: (event: { key: string; value: any; oldValue: any }) => void,
|
|
38
|
+
) {
|
|
39
|
+
this.eventBus.on(`change:${key}`, callback);
|
|
40
|
+
return {
|
|
41
|
+
dispose: () => this.eventBus.off(`change:${key}`, callback),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Listen for any configuration change.
|
|
47
|
+
*/
|
|
48
|
+
onAnyChange(
|
|
49
|
+
callback: (event: { key: string; value: any; oldValue: any }) => void,
|
|
50
|
+
) {
|
|
51
|
+
this.eventBus.on("change", callback);
|
|
52
|
+
return {
|
|
53
|
+
dispose: () => this.eventBus.off("change", callback),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Export current configuration state as a JSON-serializable object.
|
|
59
|
+
* Useful for saving configuration templates.
|
|
60
|
+
*/
|
|
61
|
+
export(): Record<string, any> {
|
|
62
|
+
const exportData: Record<string, any> = {};
|
|
63
|
+
for (const [key, value] of this.configValues) {
|
|
64
|
+
exportData[key] = value;
|
|
65
|
+
}
|
|
66
|
+
return exportData;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Import configuration from a JSON object.
|
|
71
|
+
* This will merge the provided configuration with the current state,
|
|
72
|
+
* overwriting existing keys and triggering change events.
|
|
73
|
+
*/
|
|
74
|
+
import(data: Record<string, any>): void {
|
|
75
|
+
if (!data || typeof data !== "object") {
|
|
76
|
+
console.warn("ConfigurationService: Import data must be an object.");
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
Object.entries(data).forEach(([key, value]) => {
|
|
80
|
+
this.update(key, value);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Initialize configuration with defaults from contributions.
|
|
86
|
+
* This should be called when a contribution is registered.
|
|
87
|
+
*/
|
|
88
|
+
initializeDefaults(contributions: ConfigurationContribution[]) {
|
|
89
|
+
contributions.forEach((contrib) => {
|
|
90
|
+
if (!contrib.id) {
|
|
91
|
+
console.warn(
|
|
92
|
+
"Configuration contribution missing 'id'. Skipping default initialization.",
|
|
93
|
+
contrib,
|
|
94
|
+
);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (!this.configValues.has(contrib.id) && contrib.default !== undefined) {
|
|
98
|
+
this.configValues.set(contrib.id, contrib.default);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
dispose() {
|
|
104
|
+
this.configValues.clear();
|
|
105
|
+
// EventBus doesn't have a clear/dispose in the snippet, but it's fine for now.
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { ToolContribution } from "../contribution";
|
|
2
|
+
import Disposable from "../disposable";
|
|
3
|
+
import { Service } from "../service";
|
|
4
|
+
|
|
5
|
+
export default class ToolRegistryService implements Service {
|
|
6
|
+
private tools = new Map<string, ToolContribution>();
|
|
7
|
+
|
|
8
|
+
registerTool(tool: ToolContribution): Disposable {
|
|
9
|
+
if (!tool?.id) {
|
|
10
|
+
throw new Error("ToolContribution.id is required.");
|
|
11
|
+
}
|
|
12
|
+
this.tools.set(tool.id, tool);
|
|
13
|
+
return {
|
|
14
|
+
dispose: () => {
|
|
15
|
+
if (this.tools.get(tool.id) === tool) {
|
|
16
|
+
this.tools.delete(tool.id);
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
unregisterTool(toolId: string) {
|
|
23
|
+
this.tools.delete(toolId);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
getTool(toolId: string): ToolContribution | undefined {
|
|
27
|
+
return this.tools.get(toolId);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
listTools(): ToolContribution[] {
|
|
31
|
+
return Array.from(this.tools.values());
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
hasTool(toolId: string): boolean {
|
|
35
|
+
return this.tools.has(toolId);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
dispose() {
|
|
39
|
+
this.tools.clear();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { ToolContribution } from "../contribution";
|
|
2
|
+
import Disposable from "../disposable";
|
|
3
|
+
import { Service, ServiceContext } from "../service";
|
|
4
|
+
import CommandService from "./CommandService";
|
|
5
|
+
import ToolRegistryService from "./ToolRegistryService";
|
|
6
|
+
import { COMMAND_SERVICE, TOOL_REGISTRY_SERVICE } from "./tokens";
|
|
7
|
+
|
|
8
|
+
export type ToolSessionStatus = "idle" | "active";
|
|
9
|
+
|
|
10
|
+
export interface ToolSessionState {
|
|
11
|
+
toolId: string;
|
|
12
|
+
status: ToolSessionStatus;
|
|
13
|
+
dirty: boolean;
|
|
14
|
+
startedAt?: number;
|
|
15
|
+
lastUpdatedAt?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type LeaveDecision = "allow" | "blocked";
|
|
19
|
+
|
|
20
|
+
export interface LeaveResult {
|
|
21
|
+
decision: LeaveDecision;
|
|
22
|
+
reason?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface ToolSessionServiceDependencies {
|
|
26
|
+
commandService?: CommandService;
|
|
27
|
+
toolRegistry?: ToolRegistryService;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default class ToolSessionService implements Service {
|
|
31
|
+
private readonly sessions = new Map<string, ToolSessionState>();
|
|
32
|
+
private commandService?: CommandService;
|
|
33
|
+
private toolRegistry?: ToolRegistryService;
|
|
34
|
+
|
|
35
|
+
constructor(dependencies: ToolSessionServiceDependencies = {}) {
|
|
36
|
+
this.commandService = dependencies.commandService;
|
|
37
|
+
this.toolRegistry = dependencies.toolRegistry;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
init(context: ServiceContext) {
|
|
41
|
+
this.commandService ??= context.get(COMMAND_SERVICE);
|
|
42
|
+
this.toolRegistry ??= context.get(TOOL_REGISTRY_SERVICE);
|
|
43
|
+
|
|
44
|
+
if (!this.commandService) {
|
|
45
|
+
throw new Error("ToolSessionService requires CommandService.");
|
|
46
|
+
}
|
|
47
|
+
if (!this.toolRegistry) {
|
|
48
|
+
throw new Error("ToolSessionService requires ToolRegistryService.");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
setCommandService(commandService: CommandService) {
|
|
53
|
+
this.commandService = commandService;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
setToolRegistry(toolRegistry: ToolRegistryService) {
|
|
57
|
+
this.toolRegistry = toolRegistry;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
registerDirtyTracker(toolId: string, callback: () => boolean): Disposable {
|
|
61
|
+
const wrapped = () => {
|
|
62
|
+
try {
|
|
63
|
+
return callback();
|
|
64
|
+
} catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
this.dirtyTrackers.set(toolId, wrapped);
|
|
69
|
+
return {
|
|
70
|
+
dispose: () => {
|
|
71
|
+
if (this.dirtyTrackers.get(toolId) === wrapped) {
|
|
72
|
+
this.dirtyTrackers.delete(toolId);
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private readonly dirtyTrackers = new Map<string, () => boolean>();
|
|
79
|
+
|
|
80
|
+
private ensureSession(toolId: string): ToolSessionState {
|
|
81
|
+
const existing = this.sessions.get(toolId);
|
|
82
|
+
if (existing) return existing;
|
|
83
|
+
|
|
84
|
+
const created: ToolSessionState = {
|
|
85
|
+
toolId,
|
|
86
|
+
status: "idle",
|
|
87
|
+
dirty: false,
|
|
88
|
+
};
|
|
89
|
+
this.sessions.set(toolId, created);
|
|
90
|
+
return created;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
getState(toolId: string): ToolSessionState {
|
|
94
|
+
return { ...this.ensureSession(toolId) };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
isDirty(toolId: string): boolean {
|
|
98
|
+
const tracker = this.dirtyTrackers.get(toolId);
|
|
99
|
+
if (tracker) return tracker();
|
|
100
|
+
return this.ensureSession(toolId).dirty;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
markDirty(toolId: string, dirty = true) {
|
|
104
|
+
const session = this.ensureSession(toolId);
|
|
105
|
+
session.dirty = dirty;
|
|
106
|
+
session.lastUpdatedAt = Date.now();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private resolveTool(toolId: string): ToolContribution | undefined {
|
|
110
|
+
return this.getToolRegistry().getTool(toolId);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private async runCommand(commandId: string | undefined, ...args: any[]) {
|
|
114
|
+
if (!commandId) return undefined;
|
|
115
|
+
return await this.getCommandService().executeCommand(commandId, ...args);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private getCommandService(): CommandService {
|
|
119
|
+
if (!this.commandService) {
|
|
120
|
+
throw new Error("ToolSessionService is not initialized.");
|
|
121
|
+
}
|
|
122
|
+
return this.commandService;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private getToolRegistry(): ToolRegistryService {
|
|
126
|
+
if (!this.toolRegistry) {
|
|
127
|
+
throw new Error("ToolSessionService is not initialized.");
|
|
128
|
+
}
|
|
129
|
+
return this.toolRegistry;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async begin(toolId: string): Promise<void> {
|
|
133
|
+
const tool = this.resolveTool(toolId);
|
|
134
|
+
const session = this.ensureSession(toolId);
|
|
135
|
+
if (session.status === "active") return;
|
|
136
|
+
|
|
137
|
+
await this.runCommand(tool?.commands?.begin);
|
|
138
|
+
session.status = "active";
|
|
139
|
+
session.startedAt = Date.now();
|
|
140
|
+
session.lastUpdatedAt = session.startedAt;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async validate(toolId: string): Promise<{ ok: boolean; result?: any }> {
|
|
144
|
+
const tool = this.resolveTool(toolId);
|
|
145
|
+
if (!tool?.commands?.validate) {
|
|
146
|
+
return { ok: true };
|
|
147
|
+
}
|
|
148
|
+
const result = await this.runCommand(tool.commands.validate);
|
|
149
|
+
if (result === false) return { ok: false, result };
|
|
150
|
+
if (result && typeof result === "object" && "ok" in result) {
|
|
151
|
+
return { ok: Boolean((result as any).ok), result };
|
|
152
|
+
}
|
|
153
|
+
return { ok: true, result };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async commit(toolId: string): Promise<{ ok: boolean; result?: any }> {
|
|
157
|
+
const tool = this.resolveTool(toolId);
|
|
158
|
+
const validateResult = await this.validate(toolId);
|
|
159
|
+
if (!validateResult.ok) return validateResult;
|
|
160
|
+
|
|
161
|
+
const result = await this.runCommand(tool?.commands?.commit);
|
|
162
|
+
const session = this.ensureSession(toolId);
|
|
163
|
+
session.dirty = false;
|
|
164
|
+
session.status = "idle";
|
|
165
|
+
session.lastUpdatedAt = Date.now();
|
|
166
|
+
return { ok: true, result };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async rollback(toolId: string): Promise<void> {
|
|
170
|
+
const tool = this.resolveTool(toolId);
|
|
171
|
+
await this.runCommand(tool?.commands?.rollback || tool?.commands?.reset);
|
|
172
|
+
const session = this.ensureSession(toolId);
|
|
173
|
+
session.dirty = false;
|
|
174
|
+
session.status = "idle";
|
|
175
|
+
session.lastUpdatedAt = Date.now();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
deactivateSession(toolId: string) {
|
|
179
|
+
const session = this.ensureSession(toolId);
|
|
180
|
+
session.status = "idle";
|
|
181
|
+
session.lastUpdatedAt = Date.now();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async handleBeforeLeave(toolId: string): Promise<LeaveResult> {
|
|
185
|
+
const tool = this.resolveTool(toolId);
|
|
186
|
+
if (!tool) return { decision: "allow" };
|
|
187
|
+
if (tool.interaction !== "session") return { decision: "allow" };
|
|
188
|
+
|
|
189
|
+
const dirty = this.isDirty(toolId);
|
|
190
|
+
if (!dirty) return { decision: "allow" };
|
|
191
|
+
|
|
192
|
+
const leavePolicy = tool.session?.leavePolicy ?? "block";
|
|
193
|
+
if (leavePolicy === "commit") {
|
|
194
|
+
const committed = await this.commit(toolId);
|
|
195
|
+
if (!committed.ok) {
|
|
196
|
+
return { decision: "blocked", reason: "session-validation-failed" };
|
|
197
|
+
}
|
|
198
|
+
return { decision: "allow" };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (leavePolicy === "rollback") {
|
|
202
|
+
await this.rollback(toolId);
|
|
203
|
+
return { decision: "allow" };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return { decision: "blocked", reason: "session-dirty" };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
dispose() {
|
|
210
|
+
this.sessions.clear();
|
|
211
|
+
this.dirtyTrackers.clear();
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -1,30 +1,209 @@
|
|
|
1
|
-
import
|
|
1
|
+
import Disposable from "../disposable";
|
|
2
2
|
import EventBus from "../event";
|
|
3
|
+
import { Service, ServiceContext } from "../service";
|
|
4
|
+
import ToolRegistryService from "./ToolRegistryService";
|
|
5
|
+
import ToolSessionService from "./ToolSessionService";
|
|
6
|
+
import { TOOL_REGISTRY_SERVICE, TOOL_SESSION_SERVICE } from "./tokens";
|
|
7
|
+
|
|
8
|
+
export interface ToolSwitchContext {
|
|
9
|
+
from: string | null;
|
|
10
|
+
to: string | null;
|
|
11
|
+
reason?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ToolSwitchResult {
|
|
15
|
+
ok: boolean;
|
|
16
|
+
from: string | null;
|
|
17
|
+
to: string | null;
|
|
18
|
+
reason?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type ToolSwitchGuard = (
|
|
22
|
+
context: ToolSwitchContext,
|
|
23
|
+
) => boolean | Promise<boolean>;
|
|
24
|
+
|
|
25
|
+
interface GuardItem {
|
|
26
|
+
guard: ToolSwitchGuard;
|
|
27
|
+
priority: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface WorkbenchServiceDependencies {
|
|
31
|
+
eventBus?: EventBus;
|
|
32
|
+
toolRegistry?: ToolRegistryService;
|
|
33
|
+
sessionService?: ToolSessionService;
|
|
34
|
+
}
|
|
3
35
|
|
|
4
36
|
export default class WorkbenchService implements Service {
|
|
5
37
|
private _activeToolId: string | null = null;
|
|
6
38
|
private eventBus?: EventBus;
|
|
39
|
+
private toolRegistry?: ToolRegistryService;
|
|
40
|
+
private sessionService?: ToolSessionService;
|
|
41
|
+
private guards: GuardItem[] = [];
|
|
7
42
|
|
|
8
|
-
|
|
9
|
-
|
|
43
|
+
constructor(dependencies: WorkbenchServiceDependencies = {}) {
|
|
44
|
+
this.eventBus = dependencies.eventBus;
|
|
45
|
+
this.toolRegistry = dependencies.toolRegistry;
|
|
46
|
+
this.sessionService = dependencies.sessionService;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
init(context: ServiceContext) {
|
|
50
|
+
this.eventBus ??= context.eventBus;
|
|
51
|
+
this.toolRegistry ??= context.get(TOOL_REGISTRY_SERVICE);
|
|
52
|
+
this.sessionService ??= context.get(TOOL_SESSION_SERVICE);
|
|
53
|
+
|
|
54
|
+
if (!this.eventBus) {
|
|
55
|
+
throw new Error("WorkbenchService requires EventBus.");
|
|
56
|
+
}
|
|
57
|
+
if (!this.toolRegistry) {
|
|
58
|
+
throw new Error("WorkbenchService requires ToolRegistryService.");
|
|
59
|
+
}
|
|
60
|
+
if (!this.sessionService) {
|
|
61
|
+
throw new Error("WorkbenchService requires ToolSessionService.");
|
|
62
|
+
}
|
|
10
63
|
}
|
|
11
64
|
|
|
12
65
|
dispose() {
|
|
13
|
-
|
|
66
|
+
this.guards = [];
|
|
14
67
|
}
|
|
15
68
|
|
|
16
69
|
setEventBus(bus: EventBus) {
|
|
17
70
|
this.eventBus = bus;
|
|
18
71
|
}
|
|
19
72
|
|
|
20
|
-
|
|
73
|
+
setToolRegistry(toolRegistry: ToolRegistryService) {
|
|
74
|
+
this.toolRegistry = toolRegistry;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
setToolSessionService(sessionService: ToolSessionService) {
|
|
78
|
+
this.sessionService = sessionService;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
get activeToolId(): string | null {
|
|
21
82
|
return this._activeToolId;
|
|
22
83
|
}
|
|
23
84
|
|
|
24
|
-
|
|
25
|
-
|
|
85
|
+
registerSwitchGuard(
|
|
86
|
+
guard: ToolSwitchGuard,
|
|
87
|
+
priority: number = 0,
|
|
88
|
+
): Disposable {
|
|
89
|
+
const item: GuardItem = { guard, priority };
|
|
90
|
+
this.guards.push(item);
|
|
91
|
+
this.guards.sort((a, b) => b.priority - a.priority);
|
|
92
|
+
return {
|
|
93
|
+
dispose: () => {
|
|
94
|
+
const index = this.guards.indexOf(item);
|
|
95
|
+
if (index >= 0) this.guards.splice(index, 1);
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private async runGuards(context: ToolSwitchContext): Promise<boolean> {
|
|
101
|
+
for (const { guard } of this.guards) {
|
|
102
|
+
const allowed = await Promise.resolve(guard(context));
|
|
103
|
+
if (!allowed) return false;
|
|
104
|
+
}
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async switchTool(
|
|
109
|
+
id: string | null,
|
|
110
|
+
options?: { reason?: string },
|
|
111
|
+
): Promise<ToolSwitchResult> {
|
|
112
|
+
const eventBus = this.getEventBus();
|
|
113
|
+
const toolRegistry = this.getToolRegistry();
|
|
114
|
+
const sessionService = this.getSessionService();
|
|
115
|
+
|
|
116
|
+
if (this._activeToolId === id) {
|
|
117
|
+
return { ok: true, from: this._activeToolId, to: id };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (id && !toolRegistry.hasTool(id)) {
|
|
121
|
+
return {
|
|
122
|
+
ok: false,
|
|
123
|
+
from: this._activeToolId,
|
|
124
|
+
to: id,
|
|
125
|
+
reason: `tool-not-registered:${id}`,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const context: ToolSwitchContext = {
|
|
130
|
+
from: this._activeToolId,
|
|
131
|
+
to: id,
|
|
132
|
+
reason: options?.reason,
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const guardAllowed = await this.runGuards(context);
|
|
136
|
+
if (!guardAllowed) {
|
|
137
|
+
eventBus.emit("tool:switch:blocked", {
|
|
138
|
+
...context,
|
|
139
|
+
reason: "blocked-by-guard",
|
|
140
|
+
});
|
|
141
|
+
return {
|
|
142
|
+
ok: false,
|
|
143
|
+
from: this._activeToolId,
|
|
144
|
+
to: id,
|
|
145
|
+
reason: "blocked-by-guard",
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (context.from) {
|
|
150
|
+
const leaveResult = await sessionService.handleBeforeLeave(context.from);
|
|
151
|
+
if (leaveResult.decision === "blocked") {
|
|
152
|
+
eventBus.emit("tool:switch:blocked", {
|
|
153
|
+
...context,
|
|
154
|
+
reason: leaveResult.reason || "session-blocked",
|
|
155
|
+
});
|
|
156
|
+
return {
|
|
157
|
+
ok: false,
|
|
158
|
+
from: this._activeToolId,
|
|
159
|
+
to: id,
|
|
160
|
+
reason: leaveResult.reason || "session-blocked",
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
sessionService.deactivateSession(context.from);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (id) {
|
|
167
|
+
const tool = toolRegistry.getTool(id);
|
|
168
|
+
if (tool?.interaction === "session" && tool.session?.autoBegin !== false) {
|
|
169
|
+
await sessionService.begin(id);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
26
173
|
const previous = this._activeToolId;
|
|
27
174
|
this._activeToolId = id;
|
|
28
|
-
|
|
175
|
+
const reason = options?.reason;
|
|
176
|
+
eventBus.emit("tool:activated", { id, previous, reason });
|
|
177
|
+
eventBus.emit("tool:switch", { from: previous, to: id, reason });
|
|
178
|
+
return { ok: true, from: previous, to: id };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async activate(id: string | null): Promise<ToolSwitchResult> {
|
|
182
|
+
return await this.switchTool(id, { reason: "activate" });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async deactivate(): Promise<ToolSwitchResult> {
|
|
186
|
+
return await this.switchTool(null, { reason: "deactivate" });
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private getEventBus(): EventBus {
|
|
190
|
+
if (!this.eventBus) {
|
|
191
|
+
throw new Error("WorkbenchService is not initialized.");
|
|
192
|
+
}
|
|
193
|
+
return this.eventBus;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private getToolRegistry(): ToolRegistryService {
|
|
197
|
+
if (!this.toolRegistry) {
|
|
198
|
+
throw new Error("WorkbenchService is not initialized.");
|
|
199
|
+
}
|
|
200
|
+
return this.toolRegistry;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private getSessionService(): ToolSessionService {
|
|
204
|
+
if (!this.sessionService) {
|
|
205
|
+
throw new Error("WorkbenchService is not initialized.");
|
|
206
|
+
}
|
|
207
|
+
return this.sessionService;
|
|
29
208
|
}
|
|
30
209
|
}
|