From 90c7be06de650bdd1dfc32eade0ed779feeb9a5c Mon Sep 17 00:00:00 2001 From: Root At Skic Date: Sat, 7 Nov 2026 01:25:34 +0200 Subject: [PATCH] 0.10.4: a real Sync button on every Archify page A labelled Sync button sits top right on every Archify page, also in Source, and opens the Sync menu. On a shared model it turns green and reads Shared. The title-bar icon and the pre-share status bar entry are gone, with their settings. --- CHANGELOG.md | 5 +++++ README.md | 2 +- package-lock.json | 4 ++-- package.json | 19 ++-------------- src/sync/index.ts | 16 +------------- src/sync/loader.ts | 20 ----------------- src/webview/messages.ts | 32 +++++++++++++++++++++++++++ src/webview/page.ts | 44 +++++++++++++++++++++++++++++++++++++ src/webview/source/index.ts | 2 +- test/integration/suite.ts | 2 -- 10 files changed, 88 insertions(+), 58 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01de1fb..9dab322 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 0.10.4 — 2026-11-07 + +- **A real Sync button.** Every Archify page (diagram, model, graph, registry, decision, and in Source) shows a **Sync** button top right. It opens the Sync menu: sign in, share the model, join one, set the server address. On a model that is shared it turns green and reads **Shared**. +- Removed the small title-bar icon and the status bar entry shown before anything was shared, with their settings `archify.sync.titleButton` and `archify.sync.statusBar`. A shared model still shows its live state in the status bar. + ## 0.10.3 — 2026-11-07 - **Sync button in the title bar.** Every Archify page (and `.archify` text editors) has a **Sync** button top right, next to *Open in Text Editor*: sign in, share a model, join one, set the server address. The status bar entry stays for those who show the status bar. `archify.sync.titleButton: false` hides the button. diff --git a/README.md b/README.md index 0d3abbf..3b277e7 100644 --- a/README.md +++ b/README.md @@ -140,7 +140,7 @@ Optional. Without a sync server the extension works exactly as described above a [Archify Sync](https://gitea.lego-cloud.eu/vscode-extensions/archify-vscode-ext-sync) is a small server you run yourself (one container and PostgreSQL; a Helm chart is included). With it, a model is edited by several people at once, like a shared document, and its files stay ordinary files on each person's disk, so Git, agents and every page of this extension keep working. -- **Where.** The **Sync** button in the title bar of every Archify page (top right, next to *Open in Text Editor*) opens a menu: sign in, share a model, join one, the server address, settings. The same menu is **Archify Sync** in the status bar (in workspaces with Archify files) and *Archify | Sync…* in the Command Palette. `archify.sync.titleButton: false` and `archify.sync.statusBar: false` hide them. +- **Where.** Every Archify page (diagram, model, graph, registry, decision, also in Source) has a **Sync** button top right. It opens the Sync menu: sign in, share the model, join one, the server address, settings. On a shared model it turns green and reads **Shared**. The same menu is *Archify | Sync…* in the Command Palette. - **Server address.** Set `archify.sync.server` once (Settings › Archify › Sync, or *Archify | Set Sync Server Address…*), for example `https://archify.example.com`. Put it in the workspace settings and everyone in the project uses the same server. Left empty, the address is asked for once and remembered. - **Sign in.** *Archify | Sign in to Sync Server…* uses that address (or asks for it) and signs in through the browser; the session is kept in VS Code's secret storage and shows under **Accounts** as *Archify Sync*. A new server is set up first on its `/setup` page with the code from its log. - **Share.** *Archify | Share Model…* (also on `main.archify` in the Explorer) puts the model on the server and writes a small `.archify-sync` file next to `main.archify`: the server and the model's id, no secrets. Commit it, and everyone who clones the repository and is a member opens the shared model. diff --git a/package-lock.json b/package-lock.json index 179e045..b353b1c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "archify-vscode-ext", - "version": "0.10.3", + "version": "0.10.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "archify-vscode-ext", - "version": "0.10.3", + "version": "0.10.4", "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.20.4", diff --git a/package.json b/package.json index 262f393..5cdfc50 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "archify-vscode-ext", "displayName": "Archify Diagram Viewer", "description": "Archify diagrams in VS Code. Write diagrams as .archify files in YAML: they open as the interactive diagram, with live updates, validation, templates and completion. Also previews Archify's own JSON files (*.architecture.json, …) and opens rendered Archify HTML.", - "version": "0.10.3", + "version": "0.10.4", "publisher": "root-at-skic", "license": "MIT", "sponsor": { @@ -270,11 +270,6 @@ "command": "archify.openModelGraph", "when": "archify.inModel && (archify.isDiagramSource || activeCustomEditorId == 'archify.diagram' || activeCustomEditorId == 'archify.registry' || activeCustomEditorId == 'archify.decision') && !notebookEditorFocused", "group": "navigation@61" - }, - { - "command": "archify.sync.menu", - "when": "config.archify.sync.titleButton && (archify.isDiagramSource || activeCustomEditorId == 'archify.diagram' || activeCustomEditorId == 'archify.registry' || activeCustomEditorId == 'archify.decision') && !notebookEditorFocused", - "group": "navigation@62" } ], "explorer/context": [ @@ -526,17 +521,7 @@ "scope": "window", "pattern": "^$|^https?://", "patternErrorMessage": "An address such as https://archify.example.com", - "markdownDescription": "Address of your Archify Sync server, for example `https://archify.example.com`. *Sign in*, *Share Model* and the status bar use it without asking. Set it in the workspace settings so everyone in the project uses the same server. Empty: asked once and remembered. Archify Sync is optional; see the README." - }, - "archify.sync.statusBar": { - "type": "boolean", - "default": true, - "markdownDescription": "Show **Archify Sync** in the status bar in workspaces with Archify files. Clicking it offers sign in, share, join and the server address. Shared models always show their state there." - }, - "archify.sync.titleButton": { - "type": "boolean", - "default": true, - "markdownDescription": "Show the **Archify Sync** button in the title bar of Archify pages and `.archify` editors: sign in, share, join, server address. Archify Sync is optional; without a server the button only offers to set one up." + "markdownDescription": "Address of your Archify Sync server, for example `https://archify.example.com`. The **Sync** button on Archify pages, *Sign in* and *Share Model* use it without asking. Set it in the workspace settings so everyone in the project uses the same server. Empty: asked once and remembered. Archify Sync is optional; see the README." } } }, diff --git a/src/sync/index.ts b/src/sync/index.ts index 046fe70..7265eff 100644 --- a/src/sync/index.ts +++ b/src/sync/index.ts @@ -41,8 +41,6 @@ export class SyncFeature implements vscode.Disposable, LiveProvider { private cursorTimer: ReturnType | undefined; /** Servers with a session on this machine, for the status bar before any model is shared. */ private signedIn: string[] = []; - /** Whether the workspace has Archify files; elsewhere the entry stays hidden until a model is shared. */ - private archifyHere = false; constructor( private readonly context: vscode.ExtensionContext, @@ -80,7 +78,6 @@ export class SyncFeature implements vscode.Disposable, LiveProvider { async start(): Promise { const found = await vscode.workspace.findFiles(`**/${LINK_FILE}`, SKIP, 50); await Promise.all(found.map((uri) => this.open(uri, 'open'))); - this.archifyHere = (await vscode.workspace.findFiles('**/*.{archify,archify-registry,archify-decision}', SKIP, 1)).length > 0; await this.refreshSignedIn(); } @@ -139,19 +136,8 @@ export class SyncFeature implements vscode.Disposable, LiveProvider { private render(): void { const spaces = [...this.spaces.values()]; if (!spaces.length) { + this.status.hide(); void vscode.commands.executeCommand('setContext', 'archify.sync.active', false); - // Nothing shared here yet: the way in, as before the sync bundle loaded. - if (!this.archifyHere || !vscode.workspace.getConfiguration('archify.sync').get('statusBar', true)) { - this.status.hide(); - return; - } - const hosts = this.signedIn.map((server) => new URL(server).host); - this.status.text = hosts.length ? `$(cloud) Archify Sync · ${hosts.join(', ')}` : '$(cloud) Archify Sync'; - this.status.tooltip = hosts.length - ? `Signed in to ${hosts.join(', ')}. Click to share a model, join one or sign out.` - : 'Archify Sync (optional): sign in, share a model with others or join one. Set the server under Settings › Archify › Sync.'; - this.status.backgroundColor = undefined; - this.status.show(); this.bridge?.changed(); return; } diff --git a/src/sync/loader.ts b/src/sync/loader.ts index 983d113..032b113 100644 --- a/src/sync/loader.ts +++ b/src/sync/loader.ts @@ -27,26 +27,8 @@ const COMMANDS = [ */ export function registerSync(context: vscode.ExtensionContext, models: ModelService): vscode.Disposable[] { let feature: Promise | undefined; - // Until the sync bundle loads, this entry is the way in; then the bundle's own one takes over. - const entry = vscode.window.createStatusBarItem('archify.sync', vscode.StatusBarAlignment.Left, 50); - entry.name = 'Archify Sync'; - entry.text = '$(cloud) Archify Sync'; - entry.tooltip = 'Archify Sync (optional): sign in, share a model with others or join one. Set the server under Settings › Archify › Sync.'; - entry.command = 'archify.sync.menu'; - // Only where there are Archify files: the extension also starts for JSON and HTML. - let archifyHere = false; - const showEntry = () => { - const wanted = archifyHere && !feature && vscode.workspace.getConfiguration('archify.sync').get('statusBar', true); - if (wanted) entry.show(); - else entry.hide(); - }; - void Promise.resolve(vscode.workspace.findFiles('**/*.{archify,archify-registry,archify-decision}', '{**/node_modules/**,**/.git/**}', 1)).then((found) => { - archifyHere = found.length > 0; - showEntry(); - }); const load = (): Promise => (feature ??= (async () => { - showEntry(); // A runtime path, so esbuild leaves the sync bundle out of this one. const bundle = path.join(__dirname, 'sync.js'); const mod = require(bundle) as typeof import('./index'); @@ -63,8 +45,6 @@ export function registerSync(context: vscode.ExtensionContext, models: ModelServ })(); return [ - entry, - vscode.workspace.onDidChangeConfiguration((e) => e.affectsConfiguration('archify.sync.statusBar') && showEntry()), liveBridge, ...COMMANDS.map((id) => vscode.commands.registerCommand(id, async (...args: unknown[]) => (await load()).run(id, ...args)), diff --git a/src/webview/messages.ts b/src/webview/messages.ts index 581e362..bd32b6f 100644 --- a/src/webview/messages.ts +++ b/src/webview/messages.ts @@ -17,6 +17,8 @@ export type WebviewMessage = | { type: 'showModel' } | { type: 'liveReady' } | { type: 'liveAction'; action: LiveAction } + | { type: 'syncMenu' } + | { type: 'syncReady' } | { type: 'error'; message: string }; /** Column for opening files next to a webview without covering it. */ @@ -66,6 +68,28 @@ async function openSource( vscode.window.showWarningMessage(`Archify: source file "${message.path}" was not found in the workspace.`); } +/** + * Whether the file's model is shared: an .archify-sync file next to its + * main.archify. Read from disk, so the sync bundle stays unloaded. + */ +async function isShared(file: vscode.Uri): Promise { + const folder = vscode.workspace.getWorkspaceFolder(file); + let dir = vscode.Uri.joinPath(file, '..'); + for (let depth = 0; depth < 16; depth++) { + if (await exists(vscode.Uri.joinPath(dir, '.archify-sync'))) return true; + if (await exists(vscode.Uri.joinPath(dir, 'main.archify'))) return false; + const parent = vscode.Uri.joinPath(dir, '..'); + if (!folder || dir.path === folder.uri.path || parent.path === dir.path) return false; + dir = parent; + } + return false; +} + +async function postSyncState(file: vscode.Uri, panel: vscode.WebviewPanel): Promise { + const shared = file.scheme === 'file' && (await isShared(file)); + await Promise.resolve(panel.webview.postMessage({ type: 'syncState', shared })).catch(() => undefined); +} + export async function handleWebviewMessage( message: WebviewMessage, context: vscode.Uri, @@ -99,6 +123,14 @@ export async function handleWebviewMessage( case 'error': vscode.window.showErrorMessage(`Archify: ${message.message}`); break; + case 'syncReady': + await postSyncState(context, panel); + break; + case 'syncMenu': + await vscode.commands.executeCommand('archify.sync.menu'); + // Share or Stop may have changed it. + await postSyncState(context, panel); + break; case 'hash': case 'openDiagram': case 'showGraph': diff --git a/src/webview/page.ts b/src/webview/page.ts index 6e1abf8..24434e1 100644 --- a/src/webview/page.ts +++ b/src/webview/page.ts @@ -887,6 +887,50 @@ function bridgeScript(options: PageOptions): string { onReady(renderDecisionsRail); onReady(describeElements); onReady(renderModeSwitch); + + // Archify Sync: one labelled button, top right, on every page of an Archify + // file (diagram, model, graph, registry, decision). It opens the Sync menu. + function renderSyncButton() { + if (!(config.source || config.model || config.registry || config.decision || config.graph)) return; + if (document.getElementById('archify-sync-button')) return; + var style = document.createElement('style'); + style.textContent = SYNC_BUTTON_STYLE; + document.head.appendChild(style); + var button = document.createElement('button'); + button.id = 'archify-sync-button'; + button.type = 'button'; + button.title = 'Archify Sync: sign in, share this model with others, or join one'; + button.addEventListener('click', function (event) { event.stopPropagation(); post({ type: 'syncMenu' }); }); + document.body.appendChild(button); + showSyncState(false); + post({ type: 'syncReady' }); + } + // Not shared: a cloud, "Sync". Shared (the model has an .archify-sync file): broadcast waves, "Shared". + var SYNC_ICONS = { + off: '', + on: '', + }; + function showSyncState(shared) { + var button = document.getElementById('archify-sync-button'); + if (!button) return; + button.classList.toggle('is-shared', !!shared); + button.innerHTML = (shared ? SYNC_ICONS.on : SYNC_ICONS.off) + '' + (shared ? 'Shared' : 'Sync') + ''; + button.title = shared + ? 'This model is shared with Archify Sync: invite people, publish a version, see versions' + : 'Archify Sync: sign in, share this model with others, or join one'; + } + window.addEventListener('message', function (event) { + var message = event.data || {}; + if (message.type === 'syncState') showSyncState(message.shared); + }); + var SYNC_BUTTON_STYLE = '#archify-sync-button { position: fixed; top: 12px; right: 18px; z-index: 1001; display: inline-flex; align-items: center; gap: 6px;' + + ' padding: 6px 12px; border-radius: 8px; cursor: pointer; font: 600 13px/1 var(--vscode-font-family, system-ui, sans-serif);' + + ' color: var(--vscode-button-foreground, #fff); background: var(--vscode-button-background, #0e639c); border: 1px solid var(--vscode-button-border, transparent); }' + + ' #archify-sync-button:hover { background: var(--vscode-button-hoverBackground, #1177bb); }' + + ' #archify-sync-button.is-shared { color: #fff; background: #1f8f5f; }' + + ' #archify-sync-button.is-shared:hover { background: #23a36c; }' + + ' #archify-sync-button:focus-visible { outline: 2px solid var(--vscode-focusBorder, #007fd4); outline-offset: 2px; }'; + onReady(renderSyncButton); if (config.source) { // The host answers with the problem count for the Source badge. post({ type: 'sourceReady' }); diff --git a/src/webview/source/index.ts b/src/webview/source/index.ts index 8f49b82..f90641c 100644 --- a/src/webview/source/index.ts +++ b/src/webview/source/index.ts @@ -20,7 +20,7 @@ type HostMessage = const STYLE = ` html.archify-source-on .container > :not(.header):not(#archify-source), -html.archify-source-on body > :not(.toolbar):not(.container):not(.archify-model-head):not(.archify-model-switch-row):not(#archify-source):not(script):not(style):not(.archify-modal-backdrop), +html.archify-source-on body > :not(.toolbar):not(.container):not(.archify-model-head):not(.archify-model-switch-row):not(#archify-source):not(#archify-sync-button):not(script):not(style):not(.archify-modal-backdrop), html.archify-source-on #btn-3d, html.archify-source-on .archify-vscode-notice { display: none !important; } html:not(.archify-source-on) #archify-source { display: none !important; } diff --git a/test/integration/suite.ts b/test/integration/suite.ts index 508b51e..44bb5e8 100644 --- a/test/integration/suite.ts +++ b/test/integration/suite.ts @@ -493,8 +493,6 @@ export async function run(): Promise { // No server of anyone's built in: empty until the user sets one. const config = vscode.workspace.getConfiguration('archify.sync'); assert.equal(config.get('server'), ''); - assert.equal(config.get('statusBar'), true); - assert.equal(config.get('titleButton'), true); // The extension host's module cache: dist/sync.js (Yjs, the WebSocket) is not loaded. const loaded = Object.keys(require.cache).filter((file) => file.endsWith(`${path.sep}dist${path.sep}sync.js`)); assert.deepEqual(loaded, [], 'the sync bundle is not loaded');