logisheets-logician 1.11.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 +89 -0
- package/dist/agent/loop.d.ts +101 -0
- package/dist/agent/loop.js +260 -0
- package/dist/conversation.d.ts +105 -0
- package/dist/conversation.js +10 -0
- package/dist/craft-interactions-api.d.ts +81 -0
- package/dist/craft-interactions-api.js +25 -0
- package/dist/craft-interactions-core.d.ts +14 -0
- package/dist/craft-interactions-core.js +33 -0
- package/dist/crafts/manifest.d.ts +39 -0
- package/dist/crafts/manifest.js +10 -0
- package/dist/crafts/skill-tools.d.ts +33 -0
- package/dist/crafts/skill-tools.js +146 -0
- package/dist/crafts/store.d.ts +47 -0
- package/dist/crafts/store.js +69 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +22 -0
- package/dist/index.node.js +5065 -0
- package/dist/projection.d.ts +129 -0
- package/dist/projection.js +249 -0
- package/dist/storage.d.ts +99 -0
- package/dist/storage.js +223 -0
- package/dist/tool.d.ts +137 -0
- package/dist/tool.js +57 -0
- package/dist/tools/block-ops.d.ts +30 -0
- package/dist/tools/block-ops.js +97 -0
- package/dist/tools/builder.d.ts +206 -0
- package/dist/tools/builder.js +1502 -0
- package/dist/tools/cells.d.ts +58 -0
- package/dist/tools/cells.js +263 -0
- package/dist/tools/comments.d.ts +54 -0
- package/dist/tools/comments.js +234 -0
- package/dist/tools/craft-interactions.d.ts +22 -0
- package/dist/tools/craft-interactions.js +454 -0
- package/dist/tools/edit.d.ts +55 -0
- package/dist/tools/edit.js +356 -0
- package/dist/tools/format.d.ts +58 -0
- package/dist/tools/format.js +208 -0
- package/dist/tools/history.d.ts +13 -0
- package/dist/tools/history.js +39 -0
- package/dist/tools/inspect.d.ts +93 -0
- package/dist/tools/inspect.js +488 -0
- package/dist/tools/links.d.ts +30 -0
- package/dist/tools/links.js +122 -0
- package/dist/tools/structure.d.ts +62 -0
- package/dist/tools/structure.js +174 -0
- package/dist/tools/taxonomy.d.ts +32 -0
- package/dist/tools/taxonomy.js +93 -0
- package/package.json +53 -0
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Edit tools — modify data in an existing workbook without changing
|
|
3
|
+
* structure. Use builder tools for create_sheet / create_block /
|
|
4
|
+
* set_field_rule; use these for "change this value", "fill these in",
|
|
5
|
+
* "wipe this block". Preview is a dry-run wrapper around a batch of
|
|
6
|
+
* edits, leveraging the workbook's temp-transaction machinery.
|
|
7
|
+
*/
|
|
8
|
+
import { BlockInputBuilder, DeleteRowsInBlockBuilder, isErrorMessage, } from 'logisheets-web/pure';
|
|
9
|
+
/** Narrow ToolContext.workbook to the concrete `Client` from
|
|
10
|
+
* logisheets-web. `WorkbookClient` is a type alias for `Client` —
|
|
11
|
+
* this helper hides the cast at call sites. */
|
|
12
|
+
function asClient(ctx) {
|
|
13
|
+
return ctx.workbook;
|
|
14
|
+
}
|
|
15
|
+
/** Coerce arbitrary JSON values into the string form BlockInput
|
|
16
|
+
* expects. Mirrors the helper in builder.ts; lifted here so edit
|
|
17
|
+
* handlers don't reach into builder.ts. */
|
|
18
|
+
function stringifyForBlockInput(v) {
|
|
19
|
+
if (v === null || v === undefined)
|
|
20
|
+
return '';
|
|
21
|
+
if (typeof v === 'string')
|
|
22
|
+
return v;
|
|
23
|
+
if (typeof v === 'number' || typeof v === 'boolean')
|
|
24
|
+
return String(v);
|
|
25
|
+
return JSON.stringify(v);
|
|
26
|
+
}
|
|
27
|
+
/** Submit one transaction; throw on transport or status='err'. */
|
|
28
|
+
async function commitTransaction(client, payloads, label) {
|
|
29
|
+
const tx = { payloads, undoable: true, temp: false };
|
|
30
|
+
const result = await client.handleTransaction({ transaction: tx });
|
|
31
|
+
if (isErrorMessage(result)) {
|
|
32
|
+
throw new Error(`${label}: ${result.msg}`);
|
|
33
|
+
}
|
|
34
|
+
if (result.status.type === 'err') {
|
|
35
|
+
throw new Error(`${label}: status code ${result.status.value}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const BLOCK_CELL_CHANGE_SCHEMA = {
|
|
39
|
+
type: 'object',
|
|
40
|
+
properties: {
|
|
41
|
+
block: { type: 'string' },
|
|
42
|
+
row_key: {
|
|
43
|
+
type: 'string',
|
|
44
|
+
description: "Value of the block's key column.",
|
|
45
|
+
},
|
|
46
|
+
field: { type: 'string' },
|
|
47
|
+
value: {
|
|
48
|
+
type: ['string', 'number', 'boolean', 'null'],
|
|
49
|
+
description: 'Cell value. Prefix with "=" to write a formula. null clears.',
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
required: ['block', 'row_key', 'field', 'value'],
|
|
53
|
+
};
|
|
54
|
+
export const setBlockCells = {
|
|
55
|
+
namespace: 'edit',
|
|
56
|
+
name: 'set_block_cells',
|
|
57
|
+
description: [
|
|
58
|
+
'Write one or more cells inside any block(s) in a single atomic transaction. Each change addresses a cell by (block ref name, row_key, field) — the LLM never deals with raw (sheet, row, col).',
|
|
59
|
+
'',
|
|
60
|
+
'Pass `changes` as an array; one-cell writes are just length-1 arrays. Batching is the cheap default — putting N writes in one call is one transaction, one calc pass, one undo entry.',
|
|
61
|
+
'',
|
|
62
|
+
'Rejected up-front (whole tx aborts) when any change:',
|
|
63
|
+
' - targets a non-existent block / row_key / field, or',
|
|
64
|
+
' - targets a field with a `value_formula` on its schema (engine-computed; use set_field_rule to change the rule instead).',
|
|
65
|
+
'',
|
|
66
|
+
"Value can be a literal (string / number / boolean) or a formula prefixed with '='. `null` clears the cell.",
|
|
67
|
+
].join('\n'),
|
|
68
|
+
mutates: true,
|
|
69
|
+
confirmation: 'always',
|
|
70
|
+
inputSchema: {
|
|
71
|
+
properties: {
|
|
72
|
+
changes: {
|
|
73
|
+
type: 'array',
|
|
74
|
+
items: BLOCK_CELL_CHANGE_SCHEMA,
|
|
75
|
+
minItems: 1,
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
required: ['changes'],
|
|
79
|
+
},
|
|
80
|
+
handler: async (input, ctx) => {
|
|
81
|
+
var _a;
|
|
82
|
+
const client = asClient(ctx);
|
|
83
|
+
// 1. Resolve every distinct block ref name once. One workbook-
|
|
84
|
+
// wide getAllBlocks is cheaper than N getBlockInfo by id.
|
|
85
|
+
const allRes = await client.getAllBlocks({});
|
|
86
|
+
if (isErrorMessage(allRes)) {
|
|
87
|
+
throw new Error(`getAllBlocks failed: ${allRes.msg}`);
|
|
88
|
+
}
|
|
89
|
+
const blockByName = new Map();
|
|
90
|
+
for (const b of allRes) {
|
|
91
|
+
if ((_a = b.schema) === null || _a === void 0 ? void 0 : _a.name)
|
|
92
|
+
blockByName.set(b.schema.name, b);
|
|
93
|
+
}
|
|
94
|
+
// 2. Translate each change into a BlockInput payload, failing
|
|
95
|
+
// fast on any unresolved reference. We collect ALL payloads
|
|
96
|
+
// before committing so a single bad change rejects the whole
|
|
97
|
+
// batch (matches the description's "all-or-nothing" promise).
|
|
98
|
+
const payloads = [];
|
|
99
|
+
for (let i = 0; i < input.changes.length; i++) {
|
|
100
|
+
const c = input.changes[i];
|
|
101
|
+
const block = blockByName.get(c.block);
|
|
102
|
+
if (!block) {
|
|
103
|
+
throw new Error(`changes[${i}]: no block with ref name "${c.block}"`);
|
|
104
|
+
}
|
|
105
|
+
const schema = block.schema;
|
|
106
|
+
if (!schema) {
|
|
107
|
+
throw new Error(`changes[${i}]: block "${c.block}" has no schema`);
|
|
108
|
+
}
|
|
109
|
+
const rowEntry = schema.keys.find((k) => k.key === c.row_key);
|
|
110
|
+
if (!rowEntry) {
|
|
111
|
+
throw new Error(`changes[${i}]: no row with key "${c.row_key}" in block "${c.block}"`);
|
|
112
|
+
}
|
|
113
|
+
const fieldEntry = schema.fields.find((f) => f.field === c.field);
|
|
114
|
+
if (!fieldEntry) {
|
|
115
|
+
throw new Error(`changes[${i}]: no field named "${c.field}" in block "${c.block}"`);
|
|
116
|
+
}
|
|
117
|
+
// Surface the engine-computed gate up front (the container
|
|
118
|
+
// layer would silently drop the write otherwise — bad UX for
|
|
119
|
+
// an LLM that doesn't know why nothing happened).
|
|
120
|
+
if (typeof fieldEntry.valueFormula === 'string' &&
|
|
121
|
+
fieldEntry.valueFormula.trim() !== '') {
|
|
122
|
+
throw new Error(`changes[${i}]: field "${c.field}" on block "${c.block}" has a value_formula — its cells are engine-computed. Use set_field_rule to change the rule.`);
|
|
123
|
+
}
|
|
124
|
+
payloads.push({
|
|
125
|
+
type: 'blockInput',
|
|
126
|
+
value: new BlockInputBuilder()
|
|
127
|
+
.sheetIdx(block.sheetIdx)
|
|
128
|
+
.blockId(block.blockId)
|
|
129
|
+
.row(rowEntry.idx)
|
|
130
|
+
.col(fieldEntry.idx)
|
|
131
|
+
.input(stringifyForBlockInput(c.value))
|
|
132
|
+
.build(),
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
// 3. One atomic transaction.
|
|
136
|
+
await commitTransaction(client, payloads, `set_block_cells (${payloads.length} change${payloads.length === 1 ? '' : 's'})`);
|
|
137
|
+
return {
|
|
138
|
+
data: { applied: payloads.length },
|
|
139
|
+
display: payloads.length === 1
|
|
140
|
+
? `Set ${input.changes[0].block}[${input.changes[0].row_key}].${input.changes[0].field} = ${JSON.stringify(input.changes[0].value)}.`
|
|
141
|
+
: `Applied ${payloads.length} cell writes in one transaction.`,
|
|
142
|
+
};
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
export const clearBlock = {
|
|
146
|
+
namespace: 'edit',
|
|
147
|
+
name: 'clear_block',
|
|
148
|
+
description: 'Remove all rows from a block while keeping its schema and rules intact. Useful for resetting a working sheet between scenarios. Pass dry_run=true to count without committing.',
|
|
149
|
+
mutates: true,
|
|
150
|
+
confirmation: 'destructive',
|
|
151
|
+
inputSchema: {
|
|
152
|
+
properties: {
|
|
153
|
+
block: { type: 'string' },
|
|
154
|
+
dry_run: { type: 'boolean', default: false },
|
|
155
|
+
},
|
|
156
|
+
required: ['block'],
|
|
157
|
+
},
|
|
158
|
+
handler: async (input, ctx) => {
|
|
159
|
+
const client = asClient(ctx);
|
|
160
|
+
const allRes = await client.getAllBlocks({});
|
|
161
|
+
if (isErrorMessage(allRes)) {
|
|
162
|
+
throw new Error(`getAllBlocks failed: ${allRes.msg}`);
|
|
163
|
+
}
|
|
164
|
+
const block = allRes.find((b) => { var _a; return ((_a = b.schema) === null || _a === void 0 ? void 0 : _a.name) === input.block; });
|
|
165
|
+
if (!block) {
|
|
166
|
+
throw new Error(`no block with ref name "${input.block}"`);
|
|
167
|
+
}
|
|
168
|
+
if (block.rowCnt === 0) {
|
|
169
|
+
return {
|
|
170
|
+
data: { rows_cleared: 0 },
|
|
171
|
+
display: `${input.block} already empty.`,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
if (input.dry_run) {
|
|
175
|
+
return {
|
|
176
|
+
data: { rows_cleared: block.rowCnt },
|
|
177
|
+
display: `dry_run: would clear ${block.rowCnt} rows from ${input.block}.`,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
const payload = {
|
|
181
|
+
type: 'deleteRowsInBlock',
|
|
182
|
+
value: new DeleteRowsInBlockBuilder()
|
|
183
|
+
.sheetIdx(block.sheetIdx)
|
|
184
|
+
.blockId(block.blockId)
|
|
185
|
+
.start(0)
|
|
186
|
+
.cnt(block.rowCnt)
|
|
187
|
+
.build(),
|
|
188
|
+
};
|
|
189
|
+
await commitTransaction(client, [payload], `clear_block(${input.block})`);
|
|
190
|
+
return {
|
|
191
|
+
data: { rows_cleared: block.rowCnt },
|
|
192
|
+
display: `Cleared ${block.rowCnt} row${block.rowCnt === 1 ? '' : 's'} from ${input.block}.`,
|
|
193
|
+
};
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
export const previewChanges = {
|
|
197
|
+
namespace: 'edit',
|
|
198
|
+
name: 'preview_changes',
|
|
199
|
+
description: 'Dry-run a batch of edits via the workbook\'s temp-transaction branch: returns every cell whose value would change (direct writes + cascaded formula recalcs). Nothing is committed. Use before set_block_cells when the user asks "what would happen if…".',
|
|
200
|
+
mutates: false,
|
|
201
|
+
confirmation: 'never',
|
|
202
|
+
cost: 'normal',
|
|
203
|
+
inputSchema: {
|
|
204
|
+
properties: {
|
|
205
|
+
changes: {
|
|
206
|
+
type: 'array',
|
|
207
|
+
items: BLOCK_CELL_CHANGE_SCHEMA,
|
|
208
|
+
minItems: 1,
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
required: ['changes'],
|
|
212
|
+
},
|
|
213
|
+
handler: async (input, ctx) => {
|
|
214
|
+
var _a;
|
|
215
|
+
const client = asClient(ctx);
|
|
216
|
+
// Resolve blocks for payload translation + diff annotation.
|
|
217
|
+
const allRes = await client.getAllBlocks({});
|
|
218
|
+
if (isErrorMessage(allRes)) {
|
|
219
|
+
throw new Error(`getAllBlocks failed: ${allRes.msg}`);
|
|
220
|
+
}
|
|
221
|
+
const blockByName = new Map();
|
|
222
|
+
for (const b of allRes) {
|
|
223
|
+
if ((_a = b.schema) === null || _a === void 0 ? void 0 : _a.name)
|
|
224
|
+
blockByName.set(b.schema.name, b);
|
|
225
|
+
}
|
|
226
|
+
const payloads = [];
|
|
227
|
+
for (let i = 0; i < input.changes.length; i++) {
|
|
228
|
+
const c = input.changes[i];
|
|
229
|
+
const block = blockByName.get(c.block);
|
|
230
|
+
if (!block) {
|
|
231
|
+
throw new Error(`changes[${i}]: no block with ref name "${c.block}"`);
|
|
232
|
+
}
|
|
233
|
+
const schema = block.schema;
|
|
234
|
+
if (!schema) {
|
|
235
|
+
throw new Error(`changes[${i}]: block "${c.block}" has no schema`);
|
|
236
|
+
}
|
|
237
|
+
const rowEntry = schema.keys.find((k) => k.key === c.row_key);
|
|
238
|
+
if (!rowEntry) {
|
|
239
|
+
throw new Error(`changes[${i}]: no row with key "${c.row_key}" in block "${c.block}"`);
|
|
240
|
+
}
|
|
241
|
+
const fieldEntry = schema.fields.find((f) => f.field === c.field);
|
|
242
|
+
if (!fieldEntry) {
|
|
243
|
+
throw new Error(`changes[${i}]: no field named "${c.field}" in block "${c.block}"`);
|
|
244
|
+
}
|
|
245
|
+
payloads.push({
|
|
246
|
+
type: 'blockInput',
|
|
247
|
+
value: new BlockInputBuilder()
|
|
248
|
+
.sheetIdx(block.sheetIdx)
|
|
249
|
+
.blockId(block.blockId)
|
|
250
|
+
.row(rowEntry.idx)
|
|
251
|
+
.col(fieldEntry.idx)
|
|
252
|
+
.input(stringifyForBlockInput(c.value))
|
|
253
|
+
.build(),
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
// Run inside a temp branch — toggle on, apply, snapshot diff,
|
|
257
|
+
// discard. cleanup runs in finally so a mid-flight failure
|
|
258
|
+
// doesn't leave the workbook stuck in temp mode.
|
|
259
|
+
const toggleRes = await client.toggleStatus({ useTemp: true });
|
|
260
|
+
if (isErrorMessage(toggleRes)) {
|
|
261
|
+
throw new Error(`toggleStatus failed: ${toggleRes.msg}`);
|
|
262
|
+
}
|
|
263
|
+
try {
|
|
264
|
+
const tx = { payloads, undoable: false, temp: true };
|
|
265
|
+
const result = await client.handleTransaction({ transaction: tx });
|
|
266
|
+
if (isErrorMessage(result)) {
|
|
267
|
+
throw new Error(`preview_changes: ${result.msg}`);
|
|
268
|
+
}
|
|
269
|
+
if (result.status.type === 'err') {
|
|
270
|
+
throw new Error(`preview_changes: status code ${result.status.value}`);
|
|
271
|
+
}
|
|
272
|
+
const diffRes = await client.getTempStatusChanges();
|
|
273
|
+
if (isErrorMessage(diffRes)) {
|
|
274
|
+
throw new Error(`getTempStatusChanges failed: ${diffRes.msg}`);
|
|
275
|
+
}
|
|
276
|
+
// Annotate diff entries with block context when the cell
|
|
277
|
+
// falls inside a known block.
|
|
278
|
+
const diff = diffRes.cells.map((c) => {
|
|
279
|
+
var _a, _b, _c;
|
|
280
|
+
const annot = locateInBlock(c.sheetIdx, c.row, c.col, allRes);
|
|
281
|
+
return {
|
|
282
|
+
block: (_a = annot === null || annot === void 0 ? void 0 : annot.block) !== null && _a !== void 0 ? _a : null,
|
|
283
|
+
row_key: (_b = annot === null || annot === void 0 ? void 0 : annot.row_key) !== null && _b !== void 0 ? _b : null,
|
|
284
|
+
field: (_c = annot === null || annot === void 0 ? void 0 : annot.field) !== null && _c !== void 0 ? _c : null,
|
|
285
|
+
sheet_idx: c.sheetIdx,
|
|
286
|
+
row: c.row,
|
|
287
|
+
col: c.col,
|
|
288
|
+
before: flattenValue(c.oldValue),
|
|
289
|
+
after: flattenValue(c.newValue),
|
|
290
|
+
};
|
|
291
|
+
});
|
|
292
|
+
return {
|
|
293
|
+
data: { diff },
|
|
294
|
+
display: diff.length === 0
|
|
295
|
+
? 'No cells would change.'
|
|
296
|
+
: `${diff.length} cell${diff.length === 1 ? '' : 's'} would change.`,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
finally {
|
|
300
|
+
await client.cleanupTempStatus();
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
};
|
|
304
|
+
function flattenValue(v) {
|
|
305
|
+
if (v === 'empty')
|
|
306
|
+
return null;
|
|
307
|
+
switch (v.type) {
|
|
308
|
+
case 'str':
|
|
309
|
+
return v.value;
|
|
310
|
+
case 'number':
|
|
311
|
+
return v.value;
|
|
312
|
+
case 'bool':
|
|
313
|
+
return v.value;
|
|
314
|
+
case 'error':
|
|
315
|
+
return `#ERR:${v.value}`;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
function locateInBlock(sheetIdx, row, col, blocks) {
|
|
319
|
+
for (const b of blocks) {
|
|
320
|
+
if (b.sheetIdx !== sheetIdx)
|
|
321
|
+
continue;
|
|
322
|
+
if (row < b.rowStart || row >= b.rowStart + b.rowCnt)
|
|
323
|
+
continue;
|
|
324
|
+
if (col < b.colStart || col >= b.colStart + b.colCnt)
|
|
325
|
+
continue;
|
|
326
|
+
const schema = b.schema;
|
|
327
|
+
if (!schema)
|
|
328
|
+
return undefined;
|
|
329
|
+
const key = schema.keys.find((k) => k.idx === row - b.rowStart);
|
|
330
|
+
const field = schema.fields.find((f) => f.idx === col - b.colStart);
|
|
331
|
+
if (key && field) {
|
|
332
|
+
return { block: schema.name, row_key: key.key, field: field.field };
|
|
333
|
+
}
|
|
334
|
+
return undefined;
|
|
335
|
+
}
|
|
336
|
+
return undefined;
|
|
337
|
+
}
|
|
338
|
+
// ---------------------------------------------------------------------------
|
|
339
|
+
// Design note: no `undo_redo` here.
|
|
340
|
+
//
|
|
341
|
+
// Rationale: the engine's undo stack is shared with the human user
|
|
342
|
+
// (it's what Ctrl-Z/Y drives). Letting the AI step it would silently
|
|
343
|
+
// wipe user actions that landed between AI tx's. The AI's rollback
|
|
344
|
+
// primitive is `build__checkpoint` — labelled, isolated, and itself
|
|
345
|
+
// undoable so the user can reverse an AI restore with a single Ctrl-Z.
|
|
346
|
+
// Users still get standard Ctrl-Z/Y on the canvas; that path is
|
|
347
|
+
// untouched.
|
|
348
|
+
// ---------------------------------------------------------------------------
|
|
349
|
+
// ---------------------------------------------------------------------------
|
|
350
|
+
// Bundle
|
|
351
|
+
// ---------------------------------------------------------------------------
|
|
352
|
+
export const EDIT_TOOLS = [
|
|
353
|
+
setBlockCells,
|
|
354
|
+
clearBlock,
|
|
355
|
+
previewChanges,
|
|
356
|
+
];
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format tools — cell appearance (fill, font, borders, alignment, number
|
|
3
|
+
* format) and merging. Applies a style to every cell in a range in one
|
|
4
|
+
* transaction. Colors are accepted as ordinary hex ("#1976D2" or "1976D2");
|
|
5
|
+
* the engine's two internal color formats (RGB object for fills, 8-hex ARGB
|
|
6
|
+
* string for fonts/borders) are handled here.
|
|
7
|
+
*/
|
|
8
|
+
import type { Tool } from '../tool.js';
|
|
9
|
+
interface FormatInput {
|
|
10
|
+
sheetIdx: number;
|
|
11
|
+
startRow: number;
|
|
12
|
+
startCol: number;
|
|
13
|
+
endRow: number;
|
|
14
|
+
endCol: number;
|
|
15
|
+
bold?: boolean;
|
|
16
|
+
italic?: boolean;
|
|
17
|
+
fontColor?: string;
|
|
18
|
+
fontSize?: number;
|
|
19
|
+
fill?: string;
|
|
20
|
+
align?: 'left' | 'center' | 'right';
|
|
21
|
+
valign?: 'top' | 'center' | 'bottom';
|
|
22
|
+
numberFormat?: string;
|
|
23
|
+
border?: 'thin' | 'medium' | 'thick' | 'none';
|
|
24
|
+
borderColor?: string;
|
|
25
|
+
}
|
|
26
|
+
export declare const formatCells: Tool<FormatInput, {
|
|
27
|
+
formatted: number;
|
|
28
|
+
}>;
|
|
29
|
+
export declare const mergeCells: Tool<{
|
|
30
|
+
sheetIdx: number;
|
|
31
|
+
startRow: number;
|
|
32
|
+
startCol: number;
|
|
33
|
+
endRow: number;
|
|
34
|
+
endCol: number;
|
|
35
|
+
}, {
|
|
36
|
+
merged: boolean;
|
|
37
|
+
}>;
|
|
38
|
+
export declare const unmergeCells: Tool<{
|
|
39
|
+
sheetIdx: number;
|
|
40
|
+
row: number;
|
|
41
|
+
col: number;
|
|
42
|
+
}, {
|
|
43
|
+
unmerged: boolean;
|
|
44
|
+
}>;
|
|
45
|
+
export declare const formatBrush: Tool<{
|
|
46
|
+
srcSheetIdx: number;
|
|
47
|
+
srcRow: number;
|
|
48
|
+
srcCol: number;
|
|
49
|
+
dstSheetIdx: number;
|
|
50
|
+
dstRowStart: number;
|
|
51
|
+
dstColStart: number;
|
|
52
|
+
dstRowEnd: number;
|
|
53
|
+
dstColEnd: number;
|
|
54
|
+
}, {
|
|
55
|
+
painted: number;
|
|
56
|
+
}>;
|
|
57
|
+
export declare const FORMAT_TOOLS: Tool[];
|
|
58
|
+
export {};
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format tools — cell appearance (fill, font, borders, alignment, number
|
|
3
|
+
* format) and merging. Applies a style to every cell in a range in one
|
|
4
|
+
* transaction. Colors are accepted as ordinary hex ("#1976D2" or "1976D2");
|
|
5
|
+
* the engine's two internal color formats (RGB object for fills, 8-hex ARGB
|
|
6
|
+
* string for fonts/borders) are handled here.
|
|
7
|
+
*/
|
|
8
|
+
import { isErrorMessage } from 'logisheets-web/pure';
|
|
9
|
+
function asClient(ctx) {
|
|
10
|
+
return ctx.workbook;
|
|
11
|
+
}
|
|
12
|
+
async function commit(client, payloads, label) {
|
|
13
|
+
const tx = { payloads, undoable: true, temp: false };
|
|
14
|
+
const r = await client.handleTransaction({ transaction: tx });
|
|
15
|
+
if (isErrorMessage(r))
|
|
16
|
+
throw new Error(`${label}: ${r.msg}`);
|
|
17
|
+
if (r.status.type === 'err')
|
|
18
|
+
throw new Error(`${label}: status code ${r.status.value}`);
|
|
19
|
+
}
|
|
20
|
+
/** "#RRGGBB" / "RRGGBB" / "AARRGGBB" → {red,green,blue} (fills). */
|
|
21
|
+
function hexToRgb(hex) {
|
|
22
|
+
const h = hex.replace('#', '');
|
|
23
|
+
const rgb = h.length === 8 ? h.slice(2) : h;
|
|
24
|
+
return {
|
|
25
|
+
red: parseInt(rgb.slice(0, 2), 16) || 0,
|
|
26
|
+
green: parseInt(rgb.slice(2, 4), 16) || 0,
|
|
27
|
+
blue: parseInt(rgb.slice(4, 6), 16) || 0,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** Hex → 8-digit ARGB string, no '#' (fonts/borders). */
|
|
31
|
+
function hexToArgb(hex) {
|
|
32
|
+
const h = hex.replace('#', '').toUpperCase();
|
|
33
|
+
if (h.length === 8)
|
|
34
|
+
return h;
|
|
35
|
+
if (h.length === 6)
|
|
36
|
+
return `FF${h}`;
|
|
37
|
+
return 'FF000000';
|
|
38
|
+
}
|
|
39
|
+
const MAX_RANGE = 500;
|
|
40
|
+
function buildStyle(i) {
|
|
41
|
+
const ty = {};
|
|
42
|
+
if (i.bold !== undefined)
|
|
43
|
+
ty.setFontBold = i.bold;
|
|
44
|
+
if (i.italic !== undefined)
|
|
45
|
+
ty.setFontItalic = i.italic;
|
|
46
|
+
if (i.fontColor)
|
|
47
|
+
ty.setFontColor = hexToArgb(i.fontColor);
|
|
48
|
+
if (i.fontSize !== undefined)
|
|
49
|
+
ty.setFontSize = i.fontSize;
|
|
50
|
+
if (i.fill)
|
|
51
|
+
ty.setPatternFill = { patternType: 'solid', fgColor: hexToRgb(i.fill) };
|
|
52
|
+
if (i.align || i.valign) {
|
|
53
|
+
const a = {};
|
|
54
|
+
if (i.align)
|
|
55
|
+
a.horizontal = i.align;
|
|
56
|
+
if (i.valign)
|
|
57
|
+
a.vertical = i.valign;
|
|
58
|
+
ty.setAlignment = a;
|
|
59
|
+
}
|
|
60
|
+
if (i.numberFormat)
|
|
61
|
+
ty.setNumFmt = i.numberFormat;
|
|
62
|
+
if (i.border) {
|
|
63
|
+
const style = i.border;
|
|
64
|
+
const color = i.borderColor ? hexToArgb(i.borderColor) : 'FF0B0F19';
|
|
65
|
+
ty.setTopBorderStyle = style;
|
|
66
|
+
ty.setBottomBorderStyle = style;
|
|
67
|
+
ty.setLeftBorderStyle = style;
|
|
68
|
+
ty.setRightBorderStyle = style;
|
|
69
|
+
if (i.border !== 'none') {
|
|
70
|
+
ty.setTopBorderColor = color;
|
|
71
|
+
ty.setBottomBorderColor = color;
|
|
72
|
+
ty.setLeftBorderColor = color;
|
|
73
|
+
ty.setRightBorderColor = color;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return ty;
|
|
77
|
+
}
|
|
78
|
+
export const formatCells = {
|
|
79
|
+
namespace: 'format',
|
|
80
|
+
name: 'format_cells',
|
|
81
|
+
description: [
|
|
82
|
+
'Apply formatting to every cell in a rectangular range (zero-based sheetIdx/startRow/startCol/endRow/endCol). One transaction, one undo step.',
|
|
83
|
+
'Set any of: bold, italic, fontColor (hex), fontSize, fill (hex background), align (left|center|right), valign (top|center|bottom), numberFormat (e.g. "0.00", "0%", "$#,##0.00"), border (thin|medium|thick|none) + optional borderColor (hex). Omitted fields are left unchanged.',
|
|
84
|
+
`At most ${MAX_RANGE} cells per call.`,
|
|
85
|
+
].join('\n'),
|
|
86
|
+
mutates: true,
|
|
87
|
+
confirmation: 'always',
|
|
88
|
+
inputSchema: {
|
|
89
|
+
properties: {
|
|
90
|
+
sheetIdx: { type: 'integer' },
|
|
91
|
+
startRow: { type: 'integer' },
|
|
92
|
+
startCol: { type: 'integer' },
|
|
93
|
+
endRow: { type: 'integer' },
|
|
94
|
+
endCol: { type: 'integer' },
|
|
95
|
+
bold: { type: 'boolean' },
|
|
96
|
+
italic: { type: 'boolean' },
|
|
97
|
+
fontColor: { type: 'string', description: 'Hex, e.g. "#1976D2".' },
|
|
98
|
+
fontSize: { type: 'number' },
|
|
99
|
+
fill: { type: 'string', description: 'Hex background, e.g. "#FFF3CD".' },
|
|
100
|
+
align: { type: 'string', enum: ['left', 'center', 'right'] },
|
|
101
|
+
valign: { type: 'string', enum: ['top', 'center', 'bottom'] },
|
|
102
|
+
numberFormat: { type: 'string' },
|
|
103
|
+
border: { type: 'string', enum: ['thin', 'medium', 'thick', 'none'] },
|
|
104
|
+
borderColor: { type: 'string', description: 'Hex.' },
|
|
105
|
+
},
|
|
106
|
+
required: ['sheetIdx', 'startRow', 'startCol', 'endRow', 'endCol'],
|
|
107
|
+
},
|
|
108
|
+
handler: async (input, ctx) => {
|
|
109
|
+
const startRow = Math.min(input.startRow, input.endRow);
|
|
110
|
+
const endRow = Math.max(input.startRow, input.endRow);
|
|
111
|
+
const startCol = Math.min(input.startCol, input.endCol);
|
|
112
|
+
const endCol = Math.max(input.startCol, input.endCol);
|
|
113
|
+
const count = (endRow - startRow + 1) * (endCol - startCol + 1);
|
|
114
|
+
if (count > MAX_RANGE)
|
|
115
|
+
throw new Error(`range covers ${count} cells (max ${MAX_RANGE})`);
|
|
116
|
+
const ty = buildStyle(input);
|
|
117
|
+
const payloads = [];
|
|
118
|
+
for (let row = startRow; row <= endRow; row++)
|
|
119
|
+
for (let col = startCol; col <= endCol; col++)
|
|
120
|
+
payloads.push({
|
|
121
|
+
type: 'cellStyleUpdate',
|
|
122
|
+
value: { sheetIdx: input.sheetIdx, row, col, ty },
|
|
123
|
+
});
|
|
124
|
+
await commit(asClient(ctx), payloads, 'format_cells');
|
|
125
|
+
return { data: { formatted: count }, display: `Formatted ${count} cell(s)` };
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
export const mergeCells = {
|
|
129
|
+
namespace: 'format',
|
|
130
|
+
name: 'merge_cells',
|
|
131
|
+
description: 'Merge a rectangular range of cells into one (zero-based coordinates). The top-left cell keeps its value.',
|
|
132
|
+
mutates: true,
|
|
133
|
+
confirmation: 'always',
|
|
134
|
+
inputSchema: {
|
|
135
|
+
properties: {
|
|
136
|
+
sheetIdx: { type: 'integer' },
|
|
137
|
+
startRow: { type: 'integer' },
|
|
138
|
+
startCol: { type: 'integer' },
|
|
139
|
+
endRow: { type: 'integer' },
|
|
140
|
+
endCol: { type: 'integer' },
|
|
141
|
+
},
|
|
142
|
+
required: ['sheetIdx', 'startRow', 'startCol', 'endRow', 'endCol'],
|
|
143
|
+
},
|
|
144
|
+
handler: async (input, ctx) => {
|
|
145
|
+
await commit(asClient(ctx), [{ type: 'mergeCells', value: input }], 'merge_cells');
|
|
146
|
+
return { data: { merged: true }, display: 'Merged' };
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
export const unmergeCells = {
|
|
150
|
+
namespace: 'format',
|
|
151
|
+
name: 'unmerge_cells',
|
|
152
|
+
description: 'Split a merged region back into individual cells. Pass any cell inside the merged region (zero-based).',
|
|
153
|
+
mutates: true,
|
|
154
|
+
confirmation: 'always',
|
|
155
|
+
inputSchema: {
|
|
156
|
+
properties: {
|
|
157
|
+
sheetIdx: { type: 'integer' },
|
|
158
|
+
row: { type: 'integer' },
|
|
159
|
+
col: { type: 'integer' },
|
|
160
|
+
},
|
|
161
|
+
required: ['sheetIdx', 'row', 'col'],
|
|
162
|
+
},
|
|
163
|
+
handler: async (input, ctx) => {
|
|
164
|
+
await commit(asClient(ctx), [{ type: 'splitMergedCells', value: input }], 'unmerge_cells');
|
|
165
|
+
return { data: { unmerged: true }, display: 'Unmerged' };
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
export const formatBrush = {
|
|
169
|
+
namespace: 'format',
|
|
170
|
+
name: 'format_brush',
|
|
171
|
+
description: "Copy one source cell's formatting onto a destination range (the format-painter). Only formatting is copied, not values. All coordinates zero-based.",
|
|
172
|
+
mutates: true,
|
|
173
|
+
confirmation: 'always',
|
|
174
|
+
inputSchema: {
|
|
175
|
+
properties: {
|
|
176
|
+
srcSheetIdx: { type: 'integer' },
|
|
177
|
+
srcRow: { type: 'integer' },
|
|
178
|
+
srcCol: { type: 'integer' },
|
|
179
|
+
dstSheetIdx: { type: 'integer' },
|
|
180
|
+
dstRowStart: { type: 'integer' },
|
|
181
|
+
dstColStart: { type: 'integer' },
|
|
182
|
+
dstRowEnd: { type: 'integer' },
|
|
183
|
+
dstColEnd: { type: 'integer' },
|
|
184
|
+
},
|
|
185
|
+
required: [
|
|
186
|
+
'srcSheetIdx',
|
|
187
|
+
'srcRow',
|
|
188
|
+
'srcCol',
|
|
189
|
+
'dstSheetIdx',
|
|
190
|
+
'dstRowStart',
|
|
191
|
+
'dstColStart',
|
|
192
|
+
'dstRowEnd',
|
|
193
|
+
'dstColEnd',
|
|
194
|
+
],
|
|
195
|
+
},
|
|
196
|
+
handler: async (input, ctx) => {
|
|
197
|
+
await commit(asClient(ctx), [{ type: 'cellFormatBrush', value: input }], 'format_brush');
|
|
198
|
+
const n = (Math.abs(input.dstRowEnd - input.dstRowStart) + 1) *
|
|
199
|
+
(Math.abs(input.dstColEnd - input.dstColStart) + 1);
|
|
200
|
+
return { data: { painted: n }, display: `Painted format onto ${n} cell(s)` };
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
export const FORMAT_TOOLS = [
|
|
204
|
+
formatCells,
|
|
205
|
+
mergeCells,
|
|
206
|
+
unmergeCells,
|
|
207
|
+
formatBrush,
|
|
208
|
+
];
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* History tools — undo / redo the last committed change(s). These operate on
|
|
3
|
+
* the workbook's own undo stack (the same one Ctrl-Z drives), so they reverse
|
|
4
|
+
* any edit, whether made by the user or by Watson.
|
|
5
|
+
*/
|
|
6
|
+
import type { Tool } from '../tool.js';
|
|
7
|
+
export declare const undo: Tool<Record<string, never>, {
|
|
8
|
+
undone: boolean;
|
|
9
|
+
}>;
|
|
10
|
+
export declare const redo: Tool<Record<string, never>, {
|
|
11
|
+
redone: boolean;
|
|
12
|
+
}>;
|
|
13
|
+
export declare const HISTORY_TOOLS: Tool[];
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* History tools — undo / redo the last committed change(s). These operate on
|
|
3
|
+
* the workbook's own undo stack (the same one Ctrl-Z drives), so they reverse
|
|
4
|
+
* any edit, whether made by the user or by Watson.
|
|
5
|
+
*/
|
|
6
|
+
import { isErrorMessage } from 'logisheets-web/pure';
|
|
7
|
+
function asClient(ctx) {
|
|
8
|
+
return ctx.workbook;
|
|
9
|
+
}
|
|
10
|
+
export const undo = {
|
|
11
|
+
namespace: 'history',
|
|
12
|
+
name: 'undo',
|
|
13
|
+
description: 'Undo the last committed change to the workbook. Returns whether anything was undone (false if the undo stack was empty).',
|
|
14
|
+
mutates: true,
|
|
15
|
+
// Undo/redo are inherently reversible — no confirmation needed.
|
|
16
|
+
confirmation: 'never',
|
|
17
|
+
inputSchema: { properties: {} },
|
|
18
|
+
handler: async (_input, ctx) => {
|
|
19
|
+
const r = await asClient(ctx).undo();
|
|
20
|
+
if (isErrorMessage(r))
|
|
21
|
+
throw new Error(`undo: ${r.msg}`);
|
|
22
|
+
return { data: { undone: r }, display: r ? 'Undone' : 'Nothing to undo' };
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
export const redo = {
|
|
26
|
+
namespace: 'history',
|
|
27
|
+
name: 'redo',
|
|
28
|
+
description: 'Redo the change that was just undone. Returns whether anything was redone.',
|
|
29
|
+
mutates: true,
|
|
30
|
+
confirmation: 'never',
|
|
31
|
+
inputSchema: { properties: {} },
|
|
32
|
+
handler: async (_input, ctx) => {
|
|
33
|
+
const r = await asClient(ctx).redo();
|
|
34
|
+
if (isErrorMessage(r))
|
|
35
|
+
throw new Error(`redo: ${r.msg}`);
|
|
36
|
+
return { data: { redone: r }, display: r ? 'Redone' : 'Nothing to redo' };
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
export const HISTORY_TOOLS = [undo, redo];
|