Compare commits

..
Author SHA1 Message Date
Dayuan Jiang 2418c47764 fix(edit): refuse cut-off cell XML in edit_diagram operations (#960)
While an edit streams, the preview applies the operations received so
far. Chrome's DOMParser keeps a cell whose XML is cut off, so a vertex
without its geometry reached the canvas. draw.io placed it at the origin,
shifted the page layout and compensated the scroll position, and the
browser's clamping of that compensation left the diagram out of view
once the edit finished.

Check each new_xml with getXmlSyntaxError, as the whole document
already is, so an incomplete operation is reported instead of applied.
2026-10-09 21:06:49 +09:00
Dayuan Jiang 04d34534fd ci: run the e2e tests in six shards (#959)
* ci: run the e2e tests in four shards

One job ran all 300+ tests with one worker, about 20 minutes. Four
jobs at once take a quarter each, still with one worker per job.

* ci: six e2e shards

With four, the shard of the long workspace tests took 8 minutes while
the others took 4 to 5.
2026-10-08 18:44:14 +09:00
Dayuan Jiang 17ea4811fb feat(settings): reorganize the settings dialog and the model settings (#958)
Tabs Models, General, Drawing and About, each with a fixed title row.

The models tab becomes three pages: the user's providers with one status
each and the server's models; a searchable picker of all 24 providers in
four groups; and a provider page in three steps (connection, models,
test). A new provider gets its first suggested model, and a passing test
offers to use the model in the chat. Bedrock asks for one way of signing
in, with an optional session token for temporary access keys.

The composer's model picker lists every model, marking untested and
failed ones, and can open the provider picker. A chat error that offers
model settings opens the provider the request went to.
2026-10-08 18:10:07 +09:00
3 changed files with 86 additions and 3 deletions
+9 -3
View File
@@ -43,8 +43,14 @@ jobs:
run: npm --prefix packages/mcp-server run build && npm --prefix packages/mcp-server run check-package run: npm --prefix packages/mcp-server run build && npm --prefix packages/mcp-server run check-package
e2e: e2e:
name: E2E Tests # Six jobs at once, each with a sixth of the tests and one worker: the
# same conditions per test, a fraction of the time
name: E2E Tests (${{ matrix.shard }}/6)
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4, 5, 6]
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v6
@@ -76,7 +82,7 @@ jobs:
run: npm run build run: npm run build
- name: Run E2E tests - name: Run E2E tests
run: npm run test:e2e run: npm run test:e2e -- --shard=${{ matrix.shard }}/6
env: env:
CI: true CI: true
@@ -84,6 +90,6 @@ jobs:
uses: actions/upload-artifact@v6 uses: actions/upload-artifact@v6
if: always() if: always()
with: with:
name: playwright-report name: playwright-report-${{ matrix.shard }}
path: playwright-report/ path: playwright-report/
retention-days: 7 retention-days: 7
@@ -216,6 +216,20 @@ export function applyDiagramOperations(
continue continue
} }
// A cut-off cell (its XML still streaming in a preview) would
// parse in Chrome to a cell without its geometry
const syntaxError = getXmlSyntaxError(
`<wrapper>${op.new_xml}</wrapper>`,
)
if (syntaxError) {
errors.push({
type: "update",
cellId: op.cell_id,
message: `new_xml is not well-formed XML: ${syntaxError}`,
})
continue
}
// Parse the new XML // Parse the new XML
const newDoc = parser.parseFromString( const newDoc = parser.parseFromString(
`<wrapper>${op.new_xml}</wrapper>`, `<wrapper>${op.new_xml}</wrapper>`,
@@ -268,6 +282,20 @@ export function applyDiagramOperations(
continue continue
} }
// A cut-off cell (its XML still streaming in a preview) would
// parse in Chrome to a cell without its geometry
const syntaxError = getXmlSyntaxError(
`<wrapper>${op.new_xml}</wrapper>`,
)
if (syntaxError) {
errors.push({
type: "add",
cellId: op.cell_id,
message: `new_xml is not well-formed XML: ${syntaxError}`,
})
continue
}
// Parse the new XML // Parse the new XML
const newDoc = parser.parseFromString( const newDoc = parser.parseFromString(
`<wrapper>${op.new_xml}</wrapper>`, `<wrapper>${op.new_xml}</wrapper>`,
@@ -149,3 +149,52 @@ describe("a wrapped mxCell with its wrapper's id", () => {
expect(result).toContain('label="B"') expect(result).toContain('label="B"')
}) })
}) })
describe("cut-off new_xml", () => {
// While edit_diagram streams, the last operation's XML is often
// incomplete. Chrome's DOMParser keeps the partial cell, so it must be
// refused before it reaches the diagram.
const CUT = `<mxCell id="b" value="B2" style="rounded=1;" vertex="1" parent="1">`
it("refuses an update whose XML is cut off", () => {
const { result, errors } = applyDiagramOperations(DOC, [
{ operation: "update", cell_id: "b", new_xml: CUT },
])
expect(errors).toHaveLength(1)
expect(errors[0]).toMatchObject({ type: "update", cellId: "b" })
expect(errors[0].message).toContain("not well-formed")
expect(result).toContain('value="B"')
expect(result).not.toContain("B2")
})
it("refuses an add whose XML is cut off", () => {
const { result, errors } = applyDiagramOperations(DOC, [
{
operation: "add",
cell_id: "n",
new_xml: `<mxCell id="n" value="N" vertex="1" parent="1"><mxGeometry x="1" y="2" width="3"`,
},
])
expect(errors).toHaveLength(1)
expect(errors[0]).toMatchObject({ type: "add", cellId: "n" })
expect(result).not.toContain('id="n"')
})
it("applies the complete operations before a cut-off one", () => {
const { result, errors } = applyDiagramOperations(DOC, [
{
operation: "update",
cell_id: "b",
new_xml: `<mxCell id="b" value="B3" vertex="1" parent="1"><mxGeometry as="geometry"/></mxCell>`,
},
{
operation: "add",
cell_id: "n",
new_xml: `<mxCell id="n" value="N" vertex="1" parent="1">`,
},
])
expect(errors).toHaveLength(1)
expect(result).toContain('value="B3"')
expect(result).not.toContain('id="n"')
})
})