@pooder/core 1.2.0 → 2.1.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 +109 -3
- package/dist/index.d.ts +109 -3
- package/dist/index.js +309 -2
- package/dist/index.mjs +304 -1
- package/package.json +1 -1
- package/src/contribution/points.ts +27 -3
- package/src/extension.ts +7 -0
- package/src/index.ts +22 -1
- package/src/services/ToolRegistryService.ts +41 -0
- package/src/services/ToolSessionService.ts +176 -0
- package/src/services/WorkbenchService.ts +159 -0
- package/src/services/index.ts +10 -1
- package/src/test-extension-full.ts +3 -3
package/dist/index.mjs
CHANGED
|
@@ -245,6 +245,11 @@ var ExtensionManager = class {
|
|
|
245
245
|
const commandService = this.context.services.get("CommandService");
|
|
246
246
|
return commandService.registerCommand(item.id, item.data.handler);
|
|
247
247
|
}
|
|
248
|
+
if (pointId === ContributionPointIds.TOOLS) {
|
|
249
|
+
const toolRegistry = this.context.services.get("ToolRegistryService");
|
|
250
|
+
if (!toolRegistry) return;
|
|
251
|
+
return toolRegistry.registerTool(item.data);
|
|
252
|
+
}
|
|
248
253
|
}
|
|
249
254
|
unregister(name) {
|
|
250
255
|
const extension = this.extensionRegistry.get(name);
|
|
@@ -447,6 +452,289 @@ var ConfigurationService = class {
|
|
|
447
452
|
}
|
|
448
453
|
};
|
|
449
454
|
|
|
455
|
+
// src/services/ToolRegistryService.ts
|
|
456
|
+
var ToolRegistryService = class {
|
|
457
|
+
constructor() {
|
|
458
|
+
this.tools = /* @__PURE__ */ new Map();
|
|
459
|
+
}
|
|
460
|
+
registerTool(tool) {
|
|
461
|
+
if (!(tool == null ? void 0 : tool.id)) {
|
|
462
|
+
throw new Error("ToolContribution.id is required.");
|
|
463
|
+
}
|
|
464
|
+
this.tools.set(tool.id, tool);
|
|
465
|
+
return {
|
|
466
|
+
dispose: () => {
|
|
467
|
+
if (this.tools.get(tool.id) === tool) {
|
|
468
|
+
this.tools.delete(tool.id);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
unregisterTool(toolId) {
|
|
474
|
+
this.tools.delete(toolId);
|
|
475
|
+
}
|
|
476
|
+
getTool(toolId) {
|
|
477
|
+
return this.tools.get(toolId);
|
|
478
|
+
}
|
|
479
|
+
listTools() {
|
|
480
|
+
return Array.from(this.tools.values());
|
|
481
|
+
}
|
|
482
|
+
hasTool(toolId) {
|
|
483
|
+
return this.tools.has(toolId);
|
|
484
|
+
}
|
|
485
|
+
dispose() {
|
|
486
|
+
this.tools.clear();
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
// src/services/ToolSessionService.ts
|
|
491
|
+
var ToolSessionService = class {
|
|
492
|
+
constructor() {
|
|
493
|
+
this.sessions = /* @__PURE__ */ new Map();
|
|
494
|
+
this.dirtyTrackers = /* @__PURE__ */ new Map();
|
|
495
|
+
}
|
|
496
|
+
setCommandService(commandService) {
|
|
497
|
+
this.commandService = commandService;
|
|
498
|
+
}
|
|
499
|
+
setToolRegistry(toolRegistry) {
|
|
500
|
+
this.toolRegistry = toolRegistry;
|
|
501
|
+
}
|
|
502
|
+
registerDirtyTracker(toolId, callback) {
|
|
503
|
+
const wrapped = () => {
|
|
504
|
+
try {
|
|
505
|
+
return callback();
|
|
506
|
+
} catch (e) {
|
|
507
|
+
return false;
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
this.dirtyTrackers.set(toolId, wrapped);
|
|
511
|
+
return {
|
|
512
|
+
dispose: () => {
|
|
513
|
+
if (this.dirtyTrackers.get(toolId) === wrapped) {
|
|
514
|
+
this.dirtyTrackers.delete(toolId);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
ensureSession(toolId) {
|
|
520
|
+
const existing = this.sessions.get(toolId);
|
|
521
|
+
if (existing) return existing;
|
|
522
|
+
const created = {
|
|
523
|
+
toolId,
|
|
524
|
+
status: "idle",
|
|
525
|
+
dirty: false
|
|
526
|
+
};
|
|
527
|
+
this.sessions.set(toolId, created);
|
|
528
|
+
return created;
|
|
529
|
+
}
|
|
530
|
+
getState(toolId) {
|
|
531
|
+
return { ...this.ensureSession(toolId) };
|
|
532
|
+
}
|
|
533
|
+
isDirty(toolId) {
|
|
534
|
+
const tracker = this.dirtyTrackers.get(toolId);
|
|
535
|
+
if (tracker) return tracker();
|
|
536
|
+
return this.ensureSession(toolId).dirty;
|
|
537
|
+
}
|
|
538
|
+
markDirty(toolId, dirty = true) {
|
|
539
|
+
const session = this.ensureSession(toolId);
|
|
540
|
+
session.dirty = dirty;
|
|
541
|
+
session.lastUpdatedAt = Date.now();
|
|
542
|
+
}
|
|
543
|
+
resolveTool(toolId) {
|
|
544
|
+
var _a;
|
|
545
|
+
return (_a = this.toolRegistry) == null ? void 0 : _a.getTool(toolId);
|
|
546
|
+
}
|
|
547
|
+
async runCommand(commandId, ...args) {
|
|
548
|
+
if (!commandId || !this.commandService) return void 0;
|
|
549
|
+
return await this.commandService.executeCommand(commandId, ...args);
|
|
550
|
+
}
|
|
551
|
+
async begin(toolId) {
|
|
552
|
+
var _a;
|
|
553
|
+
const tool = this.resolveTool(toolId);
|
|
554
|
+
const session = this.ensureSession(toolId);
|
|
555
|
+
if (session.status === "active") return;
|
|
556
|
+
await this.runCommand((_a = tool == null ? void 0 : tool.commands) == null ? void 0 : _a.begin);
|
|
557
|
+
session.status = "active";
|
|
558
|
+
session.startedAt = Date.now();
|
|
559
|
+
session.lastUpdatedAt = session.startedAt;
|
|
560
|
+
}
|
|
561
|
+
async validate(toolId) {
|
|
562
|
+
var _a;
|
|
563
|
+
const tool = this.resolveTool(toolId);
|
|
564
|
+
if (!((_a = tool == null ? void 0 : tool.commands) == null ? void 0 : _a.validate)) {
|
|
565
|
+
return { ok: true };
|
|
566
|
+
}
|
|
567
|
+
const result = await this.runCommand(tool.commands.validate);
|
|
568
|
+
if (result === false) return { ok: false, result };
|
|
569
|
+
if (result && typeof result === "object" && "ok" in result) {
|
|
570
|
+
return { ok: Boolean(result.ok), result };
|
|
571
|
+
}
|
|
572
|
+
return { ok: true, result };
|
|
573
|
+
}
|
|
574
|
+
async commit(toolId) {
|
|
575
|
+
var _a;
|
|
576
|
+
const tool = this.resolveTool(toolId);
|
|
577
|
+
const validateResult = await this.validate(toolId);
|
|
578
|
+
if (!validateResult.ok) return validateResult;
|
|
579
|
+
const result = await this.runCommand((_a = tool == null ? void 0 : tool.commands) == null ? void 0 : _a.commit);
|
|
580
|
+
const session = this.ensureSession(toolId);
|
|
581
|
+
session.dirty = false;
|
|
582
|
+
session.status = "idle";
|
|
583
|
+
session.lastUpdatedAt = Date.now();
|
|
584
|
+
return { ok: true, result };
|
|
585
|
+
}
|
|
586
|
+
async rollback(toolId) {
|
|
587
|
+
var _a, _b;
|
|
588
|
+
const tool = this.resolveTool(toolId);
|
|
589
|
+
await this.runCommand(((_a = tool == null ? void 0 : tool.commands) == null ? void 0 : _a.rollback) || ((_b = tool == null ? void 0 : tool.commands) == null ? void 0 : _b.reset));
|
|
590
|
+
const session = this.ensureSession(toolId);
|
|
591
|
+
session.dirty = false;
|
|
592
|
+
session.status = "idle";
|
|
593
|
+
session.lastUpdatedAt = Date.now();
|
|
594
|
+
}
|
|
595
|
+
deactivateSession(toolId) {
|
|
596
|
+
const session = this.ensureSession(toolId);
|
|
597
|
+
session.status = "idle";
|
|
598
|
+
session.lastUpdatedAt = Date.now();
|
|
599
|
+
}
|
|
600
|
+
async handleBeforeLeave(toolId) {
|
|
601
|
+
var _a, _b;
|
|
602
|
+
const tool = this.resolveTool(toolId);
|
|
603
|
+
if (!tool) return { decision: "allow" };
|
|
604
|
+
if (tool.interaction !== "session") return { decision: "allow" };
|
|
605
|
+
const dirty = this.isDirty(toolId);
|
|
606
|
+
if (!dirty) return { decision: "allow" };
|
|
607
|
+
const leavePolicy = (_b = (_a = tool.session) == null ? void 0 : _a.leavePolicy) != null ? _b : "block";
|
|
608
|
+
if (leavePolicy === "commit") {
|
|
609
|
+
const committed = await this.commit(toolId);
|
|
610
|
+
if (!committed.ok) {
|
|
611
|
+
return { decision: "blocked", reason: "session-validation-failed" };
|
|
612
|
+
}
|
|
613
|
+
return { decision: "allow" };
|
|
614
|
+
}
|
|
615
|
+
if (leavePolicy === "rollback") {
|
|
616
|
+
await this.rollback(toolId);
|
|
617
|
+
return { decision: "allow" };
|
|
618
|
+
}
|
|
619
|
+
return { decision: "blocked", reason: "session-dirty" };
|
|
620
|
+
}
|
|
621
|
+
dispose() {
|
|
622
|
+
this.sessions.clear();
|
|
623
|
+
this.dirtyTrackers.clear();
|
|
624
|
+
}
|
|
625
|
+
};
|
|
626
|
+
|
|
627
|
+
// src/services/WorkbenchService.ts
|
|
628
|
+
var WorkbenchService = class {
|
|
629
|
+
constructor() {
|
|
630
|
+
this._activeToolId = null;
|
|
631
|
+
this.guards = [];
|
|
632
|
+
}
|
|
633
|
+
init() {
|
|
634
|
+
}
|
|
635
|
+
dispose() {
|
|
636
|
+
this.guards = [];
|
|
637
|
+
}
|
|
638
|
+
setEventBus(bus) {
|
|
639
|
+
this.eventBus = bus;
|
|
640
|
+
}
|
|
641
|
+
setToolRegistry(toolRegistry) {
|
|
642
|
+
this.toolRegistry = toolRegistry;
|
|
643
|
+
}
|
|
644
|
+
setToolSessionService(sessionService) {
|
|
645
|
+
this.sessionService = sessionService;
|
|
646
|
+
}
|
|
647
|
+
get activeToolId() {
|
|
648
|
+
return this._activeToolId;
|
|
649
|
+
}
|
|
650
|
+
registerSwitchGuard(guard, priority = 0) {
|
|
651
|
+
const item = { guard, priority };
|
|
652
|
+
this.guards.push(item);
|
|
653
|
+
this.guards.sort((a, b) => b.priority - a.priority);
|
|
654
|
+
return {
|
|
655
|
+
dispose: () => {
|
|
656
|
+
const index = this.guards.indexOf(item);
|
|
657
|
+
if (index >= 0) this.guards.splice(index, 1);
|
|
658
|
+
}
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
async runGuards(context) {
|
|
662
|
+
for (const { guard } of this.guards) {
|
|
663
|
+
const allowed = await Promise.resolve(guard(context));
|
|
664
|
+
if (!allowed) return false;
|
|
665
|
+
}
|
|
666
|
+
return true;
|
|
667
|
+
}
|
|
668
|
+
async switchTool(id, options) {
|
|
669
|
+
var _a, _b, _c, _d, _e;
|
|
670
|
+
if (this._activeToolId === id) {
|
|
671
|
+
return { ok: true, from: this._activeToolId, to: id };
|
|
672
|
+
}
|
|
673
|
+
if (id && this.toolRegistry && !this.toolRegistry.hasTool(id)) {
|
|
674
|
+
return {
|
|
675
|
+
ok: false,
|
|
676
|
+
from: this._activeToolId,
|
|
677
|
+
to: id,
|
|
678
|
+
reason: `tool-not-registered:${id}`
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
const context = {
|
|
682
|
+
from: this._activeToolId,
|
|
683
|
+
to: id,
|
|
684
|
+
reason: options == null ? void 0 : options.reason
|
|
685
|
+
};
|
|
686
|
+
const guardAllowed = await this.runGuards(context);
|
|
687
|
+
if (!guardAllowed) {
|
|
688
|
+
(_a = this.eventBus) == null ? void 0 : _a.emit("tool:switch:blocked", {
|
|
689
|
+
...context,
|
|
690
|
+
reason: "blocked-by-guard"
|
|
691
|
+
});
|
|
692
|
+
return {
|
|
693
|
+
ok: false,
|
|
694
|
+
from: this._activeToolId,
|
|
695
|
+
to: id,
|
|
696
|
+
reason: "blocked-by-guard"
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
if (context.from && this.sessionService) {
|
|
700
|
+
const leaveResult = await this.sessionService.handleBeforeLeave(
|
|
701
|
+
context.from
|
|
702
|
+
);
|
|
703
|
+
if (leaveResult.decision === "blocked") {
|
|
704
|
+
(_b = this.eventBus) == null ? void 0 : _b.emit("tool:switch:blocked", {
|
|
705
|
+
...context,
|
|
706
|
+
reason: leaveResult.reason || "session-blocked"
|
|
707
|
+
});
|
|
708
|
+
return {
|
|
709
|
+
ok: false,
|
|
710
|
+
from: this._activeToolId,
|
|
711
|
+
to: id,
|
|
712
|
+
reason: leaveResult.reason || "session-blocked"
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
this.sessionService.deactivateSession(context.from);
|
|
716
|
+
}
|
|
717
|
+
if (id && this.sessionService && this.toolRegistry) {
|
|
718
|
+
const tool = this.toolRegistry.getTool(id);
|
|
719
|
+
if ((tool == null ? void 0 : tool.interaction) === "session" && ((_c = tool.session) == null ? void 0 : _c.autoBegin) !== false) {
|
|
720
|
+
await this.sessionService.begin(id);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
const previous = this._activeToolId;
|
|
724
|
+
this._activeToolId = id;
|
|
725
|
+
const reason = options == null ? void 0 : options.reason;
|
|
726
|
+
(_d = this.eventBus) == null ? void 0 : _d.emit("tool:activated", { id, previous, reason });
|
|
727
|
+
(_e = this.eventBus) == null ? void 0 : _e.emit("tool:switch", { from: previous, to: id, reason });
|
|
728
|
+
return { ok: true, from: previous, to: id };
|
|
729
|
+
}
|
|
730
|
+
async activate(id) {
|
|
731
|
+
return await this.switchTool(id, { reason: "activate" });
|
|
732
|
+
}
|
|
733
|
+
async deactivate() {
|
|
734
|
+
return await this.switchTool(null, { reason: "deactivate" });
|
|
735
|
+
}
|
|
736
|
+
};
|
|
737
|
+
|
|
450
738
|
// src/index.ts
|
|
451
739
|
var Pooder = class {
|
|
452
740
|
constructor() {
|
|
@@ -458,6 +746,17 @@ var Pooder = class {
|
|
|
458
746
|
this.registerService(commandService, "CommandService");
|
|
459
747
|
const configurationService = new ConfigurationService();
|
|
460
748
|
this.registerService(configurationService, "ConfigurationService");
|
|
749
|
+
const toolRegistryService = new ToolRegistryService();
|
|
750
|
+
this.registerService(toolRegistryService, "ToolRegistryService");
|
|
751
|
+
const toolSessionService = new ToolSessionService();
|
|
752
|
+
toolSessionService.setCommandService(commandService);
|
|
753
|
+
toolSessionService.setToolRegistry(toolRegistryService);
|
|
754
|
+
this.registerService(toolSessionService, "ToolSessionService");
|
|
755
|
+
const workbenchService = new WorkbenchService();
|
|
756
|
+
workbenchService.setEventBus(this.eventBus);
|
|
757
|
+
workbenchService.setToolRegistry(toolRegistryService);
|
|
758
|
+
workbenchService.setToolSessionService(toolSessionService);
|
|
759
|
+
this.registerService(workbenchService, "WorkbenchService");
|
|
461
760
|
const context = {
|
|
462
761
|
eventBus: this.eventBus,
|
|
463
762
|
services: {
|
|
@@ -545,8 +844,12 @@ export {
|
|
|
545
844
|
ConfigurationService,
|
|
546
845
|
ContributionPointIds,
|
|
547
846
|
ContributionRegistry,
|
|
847
|
+
event_default as EventBus,
|
|
548
848
|
ExtensionManager,
|
|
549
849
|
ExtensionRegistry,
|
|
550
850
|
Pooder,
|
|
551
|
-
ServiceRegistry
|
|
851
|
+
ServiceRegistry,
|
|
852
|
+
ToolRegistryService,
|
|
853
|
+
ToolSessionService,
|
|
854
|
+
WorkbenchService
|
|
552
855
|
};
|
package/package.json
CHANGED
|
@@ -17,12 +17,36 @@ export interface CommandContribution {
|
|
|
17
17
|
/**
|
|
18
18
|
* Tool Contribution Data Definition
|
|
19
19
|
*/
|
|
20
|
+
export type ToolInteraction = "instant" | "session" | "hybrid";
|
|
21
|
+
|
|
22
|
+
export type ToolSessionLeavePolicy = "block" | "commit" | "rollback";
|
|
23
|
+
|
|
24
|
+
export interface ToolCommandBindings {
|
|
25
|
+
execute?: string;
|
|
26
|
+
begin?: string;
|
|
27
|
+
validate?: string;
|
|
28
|
+
commit?: string;
|
|
29
|
+
rollback?: string;
|
|
30
|
+
reset?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
20
33
|
export interface ToolContribution {
|
|
21
34
|
id: string;
|
|
22
35
|
name: string;
|
|
23
|
-
description
|
|
24
|
-
|
|
25
|
-
|
|
36
|
+
description?: string;
|
|
37
|
+
icon?: string;
|
|
38
|
+
interaction: ToolInteraction;
|
|
39
|
+
parameters?: Record<string, any>;
|
|
40
|
+
commands?: ToolCommandBindings;
|
|
41
|
+
view?: {
|
|
42
|
+
id?: string;
|
|
43
|
+
type?: "sidebar" | "panel" | "editor" | "dialog";
|
|
44
|
+
location?: string;
|
|
45
|
+
};
|
|
46
|
+
session?: {
|
|
47
|
+
autoBegin?: boolean;
|
|
48
|
+
leavePolicy?: ToolSessionLeavePolicy;
|
|
49
|
+
};
|
|
26
50
|
}
|
|
27
51
|
|
|
28
52
|
/**
|
package/src/extension.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { Contribution, ContributionPointIds } from "./contribution";
|
|
|
3
3
|
import Disposable from "./disposable";
|
|
4
4
|
import CommandService from "./services/CommandService";
|
|
5
5
|
import { ConfigurationService } from "./services";
|
|
6
|
+
import ToolRegistryService from "./services/ToolRegistryService";
|
|
6
7
|
|
|
7
8
|
interface ExtensionMetadata {
|
|
8
9
|
name: string;
|
|
@@ -110,6 +111,12 @@ class ExtensionManager {
|
|
|
110
111
|
|
|
111
112
|
return commandService.registerCommand(item.id, item.data.handler);
|
|
112
113
|
}
|
|
114
|
+
if (pointId === ContributionPointIds.TOOLS) {
|
|
115
|
+
const toolRegistry =
|
|
116
|
+
this.context.services.get<ToolRegistryService>("ToolRegistryService");
|
|
117
|
+
if (!toolRegistry) return;
|
|
118
|
+
return toolRegistry.registerTool(item.data);
|
|
119
|
+
}
|
|
113
120
|
}
|
|
114
121
|
|
|
115
122
|
unregister(name: string) {
|
package/src/index.ts
CHANGED
|
@@ -8,7 +8,13 @@ import {
|
|
|
8
8
|
ContributionPointIds,
|
|
9
9
|
ContributionRegistry,
|
|
10
10
|
} from "./contribution";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
CommandService,
|
|
13
|
+
ConfigurationService,
|
|
14
|
+
ToolRegistryService,
|
|
15
|
+
ToolSessionService,
|
|
16
|
+
WorkbenchService,
|
|
17
|
+
} from "./services";
|
|
12
18
|
import { ExtensionContext } from "./context";
|
|
13
19
|
|
|
14
20
|
export * from "./extension";
|
|
@@ -16,6 +22,7 @@ export * from "./context";
|
|
|
16
22
|
export * from "./contribution";
|
|
17
23
|
export * from "./service";
|
|
18
24
|
export * from "./services";
|
|
25
|
+
export { default as EventBus } from "./event";
|
|
19
26
|
|
|
20
27
|
export class Pooder {
|
|
21
28
|
readonly eventBus: EventBus = new EventBus();
|
|
@@ -34,6 +41,20 @@ export class Pooder {
|
|
|
34
41
|
const configurationService = new ConfigurationService();
|
|
35
42
|
this.registerService(configurationService, "ConfigurationService");
|
|
36
43
|
|
|
44
|
+
const toolRegistryService = new ToolRegistryService();
|
|
45
|
+
this.registerService(toolRegistryService, "ToolRegistryService");
|
|
46
|
+
|
|
47
|
+
const toolSessionService = new ToolSessionService();
|
|
48
|
+
toolSessionService.setCommandService(commandService);
|
|
49
|
+
toolSessionService.setToolRegistry(toolRegistryService);
|
|
50
|
+
this.registerService(toolSessionService, "ToolSessionService");
|
|
51
|
+
|
|
52
|
+
const workbenchService = new WorkbenchService();
|
|
53
|
+
workbenchService.setEventBus(this.eventBus);
|
|
54
|
+
workbenchService.setToolRegistry(toolRegistryService);
|
|
55
|
+
workbenchService.setToolSessionService(toolSessionService);
|
|
56
|
+
this.registerService(workbenchService, "WorkbenchService");
|
|
57
|
+
|
|
37
58
|
// Create a restricted context for extensions
|
|
38
59
|
const context: ExtensionContext = {
|
|
39
60
|
eventBus: this.eventBus,
|
|
@@ -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,176 @@
|
|
|
1
|
+
import { ToolContribution } from "../contribution";
|
|
2
|
+
import Disposable from "../disposable";
|
|
3
|
+
import { Service } from "../service";
|
|
4
|
+
import CommandService from "./CommandService";
|
|
5
|
+
import ToolRegistryService from "./ToolRegistryService";
|
|
6
|
+
|
|
7
|
+
export type ToolSessionStatus = "idle" | "active";
|
|
8
|
+
|
|
9
|
+
export interface ToolSessionState {
|
|
10
|
+
toolId: string;
|
|
11
|
+
status: ToolSessionStatus;
|
|
12
|
+
dirty: boolean;
|
|
13
|
+
startedAt?: number;
|
|
14
|
+
lastUpdatedAt?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type LeaveDecision = "allow" | "blocked";
|
|
18
|
+
|
|
19
|
+
export interface LeaveResult {
|
|
20
|
+
decision: LeaveDecision;
|
|
21
|
+
reason?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default class ToolSessionService implements Service {
|
|
25
|
+
private readonly sessions = new Map<string, ToolSessionState>();
|
|
26
|
+
private commandService?: CommandService;
|
|
27
|
+
private toolRegistry?: ToolRegistryService;
|
|
28
|
+
|
|
29
|
+
setCommandService(commandService: CommandService) {
|
|
30
|
+
this.commandService = commandService;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
setToolRegistry(toolRegistry: ToolRegistryService) {
|
|
34
|
+
this.toolRegistry = toolRegistry;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
registerDirtyTracker(toolId: string, callback: () => boolean): Disposable {
|
|
38
|
+
const wrapped = () => {
|
|
39
|
+
try {
|
|
40
|
+
return callback();
|
|
41
|
+
} catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
this.dirtyTrackers.set(toolId, wrapped);
|
|
46
|
+
return {
|
|
47
|
+
dispose: () => {
|
|
48
|
+
if (this.dirtyTrackers.get(toolId) === wrapped) {
|
|
49
|
+
this.dirtyTrackers.delete(toolId);
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private readonly dirtyTrackers = new Map<string, () => boolean>();
|
|
56
|
+
|
|
57
|
+
private ensureSession(toolId: string): ToolSessionState {
|
|
58
|
+
const existing = this.sessions.get(toolId);
|
|
59
|
+
if (existing) return existing;
|
|
60
|
+
|
|
61
|
+
const created: ToolSessionState = {
|
|
62
|
+
toolId,
|
|
63
|
+
status: "idle",
|
|
64
|
+
dirty: false,
|
|
65
|
+
};
|
|
66
|
+
this.sessions.set(toolId, created);
|
|
67
|
+
return created;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
getState(toolId: string): ToolSessionState {
|
|
71
|
+
return { ...this.ensureSession(toolId) };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
isDirty(toolId: string): boolean {
|
|
75
|
+
const tracker = this.dirtyTrackers.get(toolId);
|
|
76
|
+
if (tracker) return tracker();
|
|
77
|
+
return this.ensureSession(toolId).dirty;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
markDirty(toolId: string, dirty = true) {
|
|
81
|
+
const session = this.ensureSession(toolId);
|
|
82
|
+
session.dirty = dirty;
|
|
83
|
+
session.lastUpdatedAt = Date.now();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private resolveTool(toolId: string): ToolContribution | undefined {
|
|
87
|
+
return this.toolRegistry?.getTool(toolId);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private async runCommand(commandId: string | undefined, ...args: any[]) {
|
|
91
|
+
if (!commandId || !this.commandService) return undefined;
|
|
92
|
+
return await this.commandService.executeCommand(commandId, ...args);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async begin(toolId: string): Promise<void> {
|
|
96
|
+
const tool = this.resolveTool(toolId);
|
|
97
|
+
const session = this.ensureSession(toolId);
|
|
98
|
+
if (session.status === "active") return;
|
|
99
|
+
|
|
100
|
+
await this.runCommand(tool?.commands?.begin);
|
|
101
|
+
session.status = "active";
|
|
102
|
+
session.startedAt = Date.now();
|
|
103
|
+
session.lastUpdatedAt = session.startedAt;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async validate(toolId: string): Promise<{ ok: boolean; result?: any }> {
|
|
107
|
+
const tool = this.resolveTool(toolId);
|
|
108
|
+
if (!tool?.commands?.validate) {
|
|
109
|
+
return { ok: true };
|
|
110
|
+
}
|
|
111
|
+
const result = await this.runCommand(tool.commands.validate);
|
|
112
|
+
if (result === false) return { ok: false, result };
|
|
113
|
+
if (result && typeof result === "object" && "ok" in result) {
|
|
114
|
+
return { ok: Boolean((result as any).ok), result };
|
|
115
|
+
}
|
|
116
|
+
return { ok: true, result };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async commit(toolId: string): Promise<{ ok: boolean; result?: any }> {
|
|
120
|
+
const tool = this.resolveTool(toolId);
|
|
121
|
+
const validateResult = await this.validate(toolId);
|
|
122
|
+
if (!validateResult.ok) return validateResult;
|
|
123
|
+
|
|
124
|
+
const result = await this.runCommand(tool?.commands?.commit);
|
|
125
|
+
const session = this.ensureSession(toolId);
|
|
126
|
+
session.dirty = false;
|
|
127
|
+
session.status = "idle";
|
|
128
|
+
session.lastUpdatedAt = Date.now();
|
|
129
|
+
return { ok: true, result };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async rollback(toolId: string): Promise<void> {
|
|
133
|
+
const tool = this.resolveTool(toolId);
|
|
134
|
+
await this.runCommand(tool?.commands?.rollback || tool?.commands?.reset);
|
|
135
|
+
const session = this.ensureSession(toolId);
|
|
136
|
+
session.dirty = false;
|
|
137
|
+
session.status = "idle";
|
|
138
|
+
session.lastUpdatedAt = Date.now();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
deactivateSession(toolId: string) {
|
|
142
|
+
const session = this.ensureSession(toolId);
|
|
143
|
+
session.status = "idle";
|
|
144
|
+
session.lastUpdatedAt = Date.now();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async handleBeforeLeave(toolId: string): Promise<LeaveResult> {
|
|
148
|
+
const tool = this.resolveTool(toolId);
|
|
149
|
+
if (!tool) return { decision: "allow" };
|
|
150
|
+
if (tool.interaction !== "session") return { decision: "allow" };
|
|
151
|
+
|
|
152
|
+
const dirty = this.isDirty(toolId);
|
|
153
|
+
if (!dirty) return { decision: "allow" };
|
|
154
|
+
|
|
155
|
+
const leavePolicy = tool.session?.leavePolicy ?? "block";
|
|
156
|
+
if (leavePolicy === "commit") {
|
|
157
|
+
const committed = await this.commit(toolId);
|
|
158
|
+
if (!committed.ok) {
|
|
159
|
+
return { decision: "blocked", reason: "session-validation-failed" };
|
|
160
|
+
}
|
|
161
|
+
return { decision: "allow" };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (leavePolicy === "rollback") {
|
|
165
|
+
await this.rollback(toolId);
|
|
166
|
+
return { decision: "allow" };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return { decision: "blocked", reason: "session-dirty" };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
dispose() {
|
|
173
|
+
this.sessions.clear();
|
|
174
|
+
this.dirtyTrackers.clear();
|
|
175
|
+
}
|
|
176
|
+
}
|