@dieulc/pi-office-bridge 0.1.0 → 0.3.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/README.md +180 -97
- package/package.json +64 -60
- package/src/active-tools.ts +79 -0
- package/src/bridge-server.ts +283 -16
- package/src/index.ts +165 -16
- package/src/office-tools.ts +39 -238
- package/src/protocol.ts +5 -2
package/README.md
CHANGED
|
@@ -1,97 +1,180 @@
|
|
|
1
|
-
# @dieulc/pi-office-bridge
|
|
2
|
-
|
|
3
|
-
Native Pi extension that lets a local Pi process drive **Excel**, **Word**, and
|
|
4
|
-
**PowerPoint** through the [pi-for-office](../add-in/README.md) task-pane
|
|
5
|
-
add-in.
|
|
6
|
-
|
|
7
|
-
Pure extension — it only uses Pi's public extension API, so **Pi core is never
|
|
8
|
-
touched** and Pi can be updated freely.
|
|
9
|
-
|
|
10
|
-
## How it works
|
|
11
|
-
|
|
12
|
-
```
|
|
13
|
-
Excel / Word / PowerPoint (pi-for-office task pane)
|
|
14
|
-
│ WebSocket ws://127.0.0.1:38617
|
|
15
|
-
▼
|
|
16
|
-
local Pi process (this extension)
|
|
17
|
-
• registers office_<host>_<op> tools
|
|
18
|
-
• proxies Office.js calls back to the pane
|
|
19
|
-
• injects pane prompts into the Pi session
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
Two flows:
|
|
23
|
-
|
|
24
|
-
1. **Tool proxy (Pi → pane):** the Pi agent calls `office_excel_read_range`,
|
|
25
|
-
`office_word_insert_text`, …; the extension forwards a `tool_call` to the
|
|
26
|
-
attached pane; the pane runs the Office.js op and answers with a
|
|
27
|
-
`tool_result`. The LLM then sees the document content **and** has Pi's full
|
|
28
|
-
system tools (bash, git, files).
|
|
29
|
-
|
|
30
|
-
2. **Pane-driven chat (pane → Pi):** the user types in the add-in sidebar; the
|
|
31
|
-
pane forwards a `user_message`; the extension injects it into the Pi session
|
|
32
|
-
and streams the assistant's final reply back to the pane.
|
|
33
|
-
|
|
34
|
-
## Install
|
|
35
|
-
|
|
36
|
-
```bash
|
|
37
|
-
pi install npm:@dieulc/pi-office-bridge
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
1
|
+
# @dieulc/pi-office-bridge
|
|
2
|
+
|
|
3
|
+
Native Pi extension that lets a local Pi process drive **Excel**, **Word**, and
|
|
4
|
+
**PowerPoint** through the [pi-for-office](../add-in/README.md) task-pane
|
|
5
|
+
add-in.
|
|
6
|
+
|
|
7
|
+
Pure extension — it only uses Pi's public extension API, so **Pi core is never
|
|
8
|
+
touched** and Pi can be updated freely.
|
|
9
|
+
|
|
10
|
+
## How it works
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
Excel / Word / PowerPoint (pi-for-office task pane)
|
|
14
|
+
│ WebSocket ws://127.0.0.1:38617
|
|
15
|
+
▼
|
|
16
|
+
local Pi process (this extension)
|
|
17
|
+
• registers office_<host>_<op> tools
|
|
18
|
+
• proxies Office.js calls back to the pane
|
|
19
|
+
• injects pane prompts into the Pi session
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Two flows:
|
|
23
|
+
|
|
24
|
+
1. **Tool proxy (Pi → pane):** the Pi agent calls `office_excel_read_range`,
|
|
25
|
+
`office_word_insert_text`, …; the extension forwards a `tool_call` to the
|
|
26
|
+
attached pane; the pane runs the Office.js op and answers with a
|
|
27
|
+
`tool_result`. The LLM then sees the document content **and** has Pi's full
|
|
28
|
+
system tools (bash, git, files).
|
|
29
|
+
|
|
30
|
+
2. **Pane-driven chat (pane → Pi):** the user types in the add-in sidebar; the
|
|
31
|
+
pane forwards a `user_message`; the extension injects it into the Pi session
|
|
32
|
+
and streams the assistant's final reply back to the pane.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pi install npm:@dieulc/pi-office-bridge
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**Update to the latest** (the add-in's `/health` probe and version display
|
|
41
|
+
require ≥ 0.2.0):
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pi install npm:@dieulc/pi-office-bridge@latest
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Then restart Pi. Note that `pi install` pins the version it fetched into
|
|
48
|
+
`~/.pi/agent/npm/package.json`, so Pi will **not** auto-upgrade — re-run the
|
|
49
|
+
command above to get the newest bridge.
|
|
50
|
+
|
|
51
|
+
Or from the monorepo (development):
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
cd packages/bridge-extension
|
|
55
|
+
npm install
|
|
56
|
+
# then load it in pi for a quick test:
|
|
57
|
+
pi -e ./src/index.ts
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Enable in the add-in
|
|
61
|
+
|
|
62
|
+
1. Install/run the bridge so a Pi process with this extension is listening
|
|
63
|
+
(see above). Keep that Pi process running in the background.
|
|
64
|
+
2. Open pi-for-office in Excel / Word / PowerPoint.
|
|
65
|
+
3. Go to **Settings → Connections → Local Pi agent (advanced)** and flip the
|
|
66
|
+
**Enable local Pi agent** toggle on. The card shows the live connection
|
|
67
|
+
state (Connecting… → Connected); no taskpane reload is needed.
|
|
68
|
+
4. Verify with the card's **Test connection** button, or from a terminal:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
curl http://127.0.0.1:38617/health
|
|
72
|
+
# → { "ok": true, "service": "pi-office-bridge", "serverVersion": "0.2.0",
|
|
73
|
+
# "capabilities": ["http-health"], "panes": [ … ] }
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`/health` lists the attached pane(s) and their host app (excel / word /
|
|
77
|
+
powerpoint), so it doubles as a quick host-detection check. The add-in's
|
|
78
|
+
probe classifies the response (current / older bridge / timeout / browser
|
|
79
|
+
blocked) instead of reporting a bare failure — see
|
|
80
|
+
[`docs/local-development.md`](../../docs/local-development.md).
|
|
81
|
+
|
|
82
|
+
## Version & capabilities
|
|
83
|
+
|
|
84
|
+
Every `welcome` frame (and `GET /health`) advertises additive server
|
|
85
|
+
metadata:
|
|
86
|
+
|
|
87
|
+
- `serverVersion` — this package's version (e.g. `"0.2.0"`).
|
|
88
|
+
- `capabilities` — `["http-health"]` means the HTTP `/health` surface is
|
|
89
|
+
served.
|
|
90
|
+
|
|
91
|
+
Clients (the add-in card) treat an absent `capabilities` as “legacy bridge
|
|
92
|
+
(< 0.2.0)” and tell the user to update instead of claiming the bridge is
|
|
93
|
+
down. The connection state is surfaced live to the Pi TUI as soon as a pane
|
|
94
|
+
attaches or detaches (`onPanesChanged`).
|
|
95
|
+
|
|
96
|
+
## Commands
|
|
97
|
+
|
|
98
|
+
| Command | Description |
|
|
99
|
+
|---------|-------------|
|
|
100
|
+
| `/office` | Show bridge status: port + attached apps (Excel/Word/PowerPoint) |
|
|
101
|
+
| `/office-tools` | List every `office_*` tool registered |
|
|
102
|
+
|
|
103
|
+
## Configuration
|
|
104
|
+
|
|
105
|
+
- **Port** — flag `--office-bridge-port <port>` or env `PI_OFFICE_BRIDGE_PORT`
|
|
106
|
+
(default `38617`). The add-in connects to the same default; change both if you
|
|
107
|
+
override it (the add-in's bridge card has a **Bridge URL** row that both the
|
|
108
|
+
WebSocket client and the probe use). If the port is already taken by another
|
|
109
|
+
Pi process, the extension reports `EADDRINUSE` with the override hint.
|
|
110
|
+
- **Allowed origins** — env `PI_OFFICE_BRIDGE_ALLOWED_ORIGINS` (comma-separated)
|
|
111
|
+
extends the browser origins allowed to read `GET /health`. Defaults cover the
|
|
112
|
+
dev Vite server (`https://localhost:3141`) and the hosted GitHub Pages add-in
|
|
113
|
+
(`https://dieuluucanh.github.io`). The pane's WebSocket connection is
|
|
114
|
+
loopback-only and is not restricted by this list.
|
|
115
|
+
|
|
116
|
+
## Office tools
|
|
117
|
+
|
|
118
|
+
The extension registers a `office_<host>_<op>` tool per op in the shared
|
|
119
|
+
catalog. The catalog is the single source of truth: it lives in
|
|
120
|
+
`@dieulc/pi-office-protocol` (`office-catalog.ts`) and BOTH the Pi extension
|
|
121
|
+
and the add-in derive from it — the Pi side here, and the pane's bridge op
|
|
122
|
+
registry (`packages/add-in/src/bridge/`) there. Op ids are namespaced by host:
|
|
123
|
+
|
|
124
|
+
| Host | Ops |
|
|
125
|
+
| --- | --- |
|
|
126
|
+
| Excel | `get_overview`, `read_range`, `write_cells`, `fill_formula`, `search_workbook`, `modify_structure`, `format_cells`, `conditional_format`, `charts`, `trace_dependencies`, `explain_formula`, `view_settings`, `comments`, `workbook_history` |
|
|
127
|
+
| Word | `get_overview`, `read_document`, `insert_text`, `replace_text`, `format_range`, `insert_blocks`, `insert_table`, `insert_page_break`, `insert_image`, `insert_hyperlink` |
|
|
128
|
+
| PowerPoint | `get_overview`, `read_slide`, `add_slide`, `add_text_box`, `format_slide` |
|
|
129
|
+
|
|
130
|
+
### Active-tool reconciliation
|
|
131
|
+
|
|
132
|
+
The office tools are registered at `session_start` from the catalog, but only
|
|
133
|
+
**the ops the currently attached pane advertises are kept active** in the Pi
|
|
134
|
+
session (`pi.setActiveTools()`). Opening an app activates that host's tools;
|
|
135
|
+
closing it deactivates them; everything else stays untouched. Panes that don't
|
|
136
|
+
advertise an `ops` list (legacy 0.2.x clients) are given only the v1 op set.
|
|
137
|
+
This keeps the agent's prompt small and focused on the app actually open.
|
|
138
|
+
|
|
139
|
+
### Capability handshake
|
|
140
|
+
|
|
141
|
+
Panes send `hello.ops` + `hello.catalogVersion` with the op ids they can
|
|
142
|
+
execute. The server validates them against its own catalog (entries that don't
|
|
143
|
+
belong to the pane's host, or that the server doesn't know, are dropped and
|
|
144
|
+
counted). `callOfficeTool` then rejects any op the pane did not advertise with
|
|
145
|
+
an actionable message, so a mismatched add-in/bridge pair fails loudly instead
|
|
146
|
+
of silently.
|
|
147
|
+
|
|
148
|
+
- `/office` shows each attached pane's host, op count, catalog version, and any
|
|
149
|
+
ignored-op count.
|
|
150
|
+
- `/office-tools` lists every registered tool + catalog version.
|
|
151
|
+
- `GET /health` exposes `catalogVersion` and per-pane `ops` / `catalogVersion`.
|
|
152
|
+
- `before_agent_start` appends a pane-context block (attached host, "the
|
|
153
|
+
office_* tools edit the live document; formatting is fully supported; never
|
|
154
|
+
emit HTML for Word") so the agent uses the tools directly.
|
|
155
|
+
|
|
156
|
+
The pane-side executors are thin delegates to the same local tool factories the
|
|
157
|
+
browser-only path uses (see `packages/add-in/src/bridge/`), and a parity test
|
|
158
|
+
(`packages/add-in/tests/bridge-catalog-parity.test.ts`) fails CI if the pane
|
|
159
|
+
registry ever drifts from the shared catalog.
|
|
160
|
+
|
|
161
|
+
## Development
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
npm run typecheck # typecheck against @earendil-works/pi-coding-agent 0.85.x
|
|
165
|
+
npm run build # emit dist/ (for the node smoke tests)
|
|
166
|
+
npm test # smoke test + end-to-end interop test (real client ↔ real server)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
`tests/pane-interop.mjs` wires the **real** add-in `PaneBridgeClient` to the
|
|
170
|
+
**real** bridge server through the shared `@dieulc/pi-office-protocol` package —
|
|
171
|
+
the strongest proof the two halves agree on the wire format.
|
|
172
|
+
|
|
173
|
+
## Protocol
|
|
174
|
+
|
|
175
|
+
The wire protocol is shared in `@dieulc/pi-office-protocol`
|
|
176
|
+
(`packages/protocol`). Bump `BRIDGE_PROTOCOL_VERSION` on breaking changes.
|
|
177
|
+
|
|
178
|
+
## License
|
|
179
|
+
|
|
180
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,60 +1,64 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@dieulc/pi-office-bridge",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Native Pi extension — WebSocket bridge + Office tool proxy so Pi can drive Excel, Word, and PowerPoint through the pi-for-office add-in.",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"license": "MIT",
|
|
7
|
-
"repository": {
|
|
8
|
-
"type": "git",
|
|
9
|
-
"url": "https://github.com/dieuluucanh/pi-for-office"
|
|
10
|
-
},
|
|
11
|
-
"keywords": [
|
|
12
|
-
"pi",
|
|
13
|
-
"pi-package",
|
|
14
|
-
"office",
|
|
15
|
-
"excel",
|
|
16
|
-
"word",
|
|
17
|
-
"powerpoint",
|
|
18
|
-
"bridge",
|
|
19
|
-
"extension"
|
|
20
|
-
],
|
|
21
|
-
"dependencies": {
|
|
22
|
-
"@dieulc/pi-office-protocol": "
|
|
23
|
-
"typebox": "^1.3.10",
|
|
24
|
-
"ws": "^8.18.0"
|
|
25
|
-
},
|
|
26
|
-
"peerDependencies": {
|
|
27
|
-
"@earendil-works/pi-coding-agent": ">=0.85.0",
|
|
28
|
-
"@earendil-works/pi-ai": ">=0.83.0"
|
|
29
|
-
},
|
|
30
|
-
"devDependencies": {
|
|
31
|
-
"@earendil-works/pi-ai": "0.85.1",
|
|
32
|
-
"@earendil-works/pi-coding-agent": "0.85.1",
|
|
33
|
-
"@types/node": "^22.20.0",
|
|
34
|
-
"@types/ws": "^8.5.13",
|
|
35
|
-
"typescript": "^5.9.0"
|
|
36
|
-
},
|
|
37
|
-
"files": [
|
|
38
|
-
"src/index.ts",
|
|
39
|
-
"src/bridge-server.ts",
|
|
40
|
-
"src/office-tools.ts",
|
|
41
|
-
"src/
|
|
42
|
-
"
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
"
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@dieulc/pi-office-bridge",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Native Pi extension — WebSocket bridge + Office tool proxy so Pi can drive Excel, Word, and PowerPoint through the pi-for-office add-in.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/dieuluucanh/pi-for-office"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"pi",
|
|
13
|
+
"pi-package",
|
|
14
|
+
"office",
|
|
15
|
+
"excel",
|
|
16
|
+
"word",
|
|
17
|
+
"powerpoint",
|
|
18
|
+
"bridge",
|
|
19
|
+
"extension"
|
|
20
|
+
],
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@dieulc/pi-office-protocol": "^0.3.0",
|
|
23
|
+
"typebox": "^1.3.10",
|
|
24
|
+
"ws": "^8.18.0"
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"@earendil-works/pi-coding-agent": ">=0.85.0",
|
|
28
|
+
"@earendil-works/pi-ai": ">=0.83.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@earendil-works/pi-ai": "0.85.1",
|
|
32
|
+
"@earendil-works/pi-coding-agent": "0.85.1",
|
|
33
|
+
"@types/node": "^22.20.0",
|
|
34
|
+
"@types/ws": "^8.5.13",
|
|
35
|
+
"typescript": "^5.9.0"
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"src/index.ts",
|
|
39
|
+
"src/bridge-server.ts",
|
|
40
|
+
"src/office-tools.ts",
|
|
41
|
+
"src/active-tools.ts",
|
|
42
|
+
"src/protocol.ts",
|
|
43
|
+
"README.md"
|
|
44
|
+
],
|
|
45
|
+
"pi": {
|
|
46
|
+
"extensions": [
|
|
47
|
+
"./src/index.ts"
|
|
48
|
+
]
|
|
49
|
+
},
|
|
50
|
+
"scripts": {
|
|
51
|
+
"typecheck": "tsc --noEmit",
|
|
52
|
+
"build": "tsc -p tsconfig.build.json",
|
|
53
|
+
"test:active": "node --test tests/active-tools.test.mjs",
|
|
54
|
+
"test:smoke": "npm run build && node tests/smoke.mjs",
|
|
55
|
+
"test:interop": "npm run build && node --import ./tests/register-ts-loader.mjs tests/pane-interop.mjs",
|
|
56
|
+
"test": "npm run test:active && npm run test:smoke && npm run test:interop"
|
|
57
|
+
},
|
|
58
|
+
"publishConfig": {
|
|
59
|
+
"access": "public"
|
|
60
|
+
},
|
|
61
|
+
"engines": {
|
|
62
|
+
"node": ">=20"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Active office tool reconciliation — pure functions that decide which
|
|
3
|
+
* `office_<host>_<op>` tools Pi exposes based on the currently attached panes.
|
|
4
|
+
*
|
|
5
|
+
* Kept free of Pi API calls so it is unit-testable without a live Pi session.
|
|
6
|
+
*
|
|
7
|
+
* Rules:
|
|
8
|
+
* - Office tool names are identified from the shared catalog.
|
|
9
|
+
* - A pane advertising `ops` exposes exactly those ops' tools.
|
|
10
|
+
* - A legacy pane (no `ops`) exposes only the v1 op set.
|
|
11
|
+
* - Tools for hosts that are no longer attached are deactivated.
|
|
12
|
+
* - Non-office tools in the current active set are preserved untouched.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
CATALOG_VERSION,
|
|
17
|
+
LEGACY_V1_OPS,
|
|
18
|
+
OFFICE_CATALOG_BY_OP,
|
|
19
|
+
OFFICE_TOOL_NAMES,
|
|
20
|
+
officeToolName,
|
|
21
|
+
} from "./protocol.js";
|
|
22
|
+
import type { OfficeHostApp } from "./protocol.js";
|
|
23
|
+
|
|
24
|
+
/** What Pi knows about a pane's capabilities (server-normalized). */
|
|
25
|
+
export interface PaneCapability {
|
|
26
|
+
host: OfficeHostApp;
|
|
27
|
+
/** Ops this pane advertises, or null for legacy panes (v1 set only). */
|
|
28
|
+
ops: readonly string[] | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The catalog version the pane's ops were derived from, when advertised. */
|
|
32
|
+
export type { CATALOG_VERSION };
|
|
33
|
+
|
|
34
|
+
/** Office tool names from the catalog, as a set for O(1) membership. */
|
|
35
|
+
const OFFICE_TOOL_NAME_SET: ReadonlySet<string> = new Set(OFFICE_TOOL_NAMES);
|
|
36
|
+
|
|
37
|
+
/** True when the name is one of the catalog's office_* tools. */
|
|
38
|
+
export function isOfficeToolName(name: string): boolean {
|
|
39
|
+
return OFFICE_TOOL_NAME_SET.has(name);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The pi tool names enabled by the given panes (host-scoped, deterministic
|
|
44
|
+
* order). Legacy panes without an `ops` list get the v1 op set only.
|
|
45
|
+
*/
|
|
46
|
+
export function activeOfficeToolNames(
|
|
47
|
+
panes: readonly PaneCapability[],
|
|
48
|
+
): string[] {
|
|
49
|
+
const names = new Set<string>();
|
|
50
|
+
for (const pane of panes) {
|
|
51
|
+
const ops = pane.ops ?? LEGACY_V1_OPS;
|
|
52
|
+
for (const op of ops) {
|
|
53
|
+
const entry = OFFICE_CATALOG_BY_OP.get(op);
|
|
54
|
+
if (entry && entry.host === pane.host) {
|
|
55
|
+
names.add(officeToolName(entry.host, entry.op));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return [...names].sort((a, b) => a.localeCompare(b));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Compute the next active tool list: drop all office tools, then re-add the
|
|
64
|
+
* office tools for the currently attached panes, preserving the caller's
|
|
65
|
+
* existing non-office tools. Deterministic and idempotent.
|
|
66
|
+
*/
|
|
67
|
+
export function reconcileOfficeToolActivation(
|
|
68
|
+
currentActive: readonly string[],
|
|
69
|
+
panes: readonly PaneCapability[],
|
|
70
|
+
): string[] {
|
|
71
|
+
const next = currentActive.filter((name) => !isOfficeToolName(name));
|
|
72
|
+
const toAdd = activeOfficeToolNames(panes);
|
|
73
|
+
return [...new Set([...next, ...toAdd])].sort((a, b) => {
|
|
74
|
+
const aOffice = isOfficeToolName(a) ? 1 : 0;
|
|
75
|
+
const bOffice = isOfficeToolName(b) ? 1 : 0;
|
|
76
|
+
if (aOffice !== bOffice) return aOffice - bOffice;
|
|
77
|
+
return a.localeCompare(b);
|
|
78
|
+
});
|
|
79
|
+
}
|