@mirrowel/opencode-openshare 0.3.0-dev.1 → 0.3.0-dev.10
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 +28 -2
- package/ci/openshare.js +35 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.js +11 -0
- package/dist/core.js +50 -0
- package/dist/settings.js +2 -2
- package/dist/tui.js +9 -1
- package/package.json +2 -4
- package/src/config.ts +12 -2
- package/src/core.ts +45 -0
- package/src/settings.ts +2 -2
package/README.md
CHANGED
|
@@ -1,8 +1,28 @@
|
|
|
1
1
|
# Open(code)Share plugin (`@mirrowel/opencode-openshare`)
|
|
2
2
|
|
|
3
|
-
The opencode side of [Open(code)Share](../README.md)
|
|
3
|
+
The opencode side of [Open(code)Share](../README.md) - session sharing for
|
|
4
4
|
OpenCode v1 and v2, self-hosted on your own worker.
|
|
5
5
|
|
|
6
|
+
**This package is only the plugin.** It captures and uploads sessions, but a
|
|
7
|
+
share needs a backend to upload to. OpenShare ships as four parts that
|
|
8
|
+
version and release together:
|
|
9
|
+
|
|
10
|
+
| Part | What it does | Where to get it |
|
|
11
|
+
|---|---|---|
|
|
12
|
+
| **This plugin** | hooks into opencode, syncs sessions | this npm package |
|
|
13
|
+
| **Worker** | the Cloudflare backend - stores shares, serves the viewer | deploy from the repo: `share-worker/` (3-command setup, see the repo README) |
|
|
14
|
+
| **Server** | self-hosted alternative to the worker - one Bun process, SQLite, Docker image | repo `share-server/` |
|
|
15
|
+
| **Viewer** | the share page - ships inside the worker/server assets; nothing to install separately | repo `share-viewer/` (embedded automatically) |
|
|
16
|
+
|
|
17
|
+
The repo also builds **offline snapshots** - a single self-contained HTML
|
|
18
|
+
file of any share, generated by the viewer's download button or the
|
|
19
|
+
`osh-snapshot` CLI - so a shared link keeps working even with no backend at
|
|
20
|
+
all.
|
|
21
|
+
|
|
22
|
+
Everything lives in one repository: **https://github.com/Mirrowel/openshare**
|
|
23
|
+
(clone it, or browse the [README](https://github.com/Mirrowel/openshare#readme)
|
|
24
|
+
for the full picture - deployment, configuration, and the viewer tour).
|
|
25
|
+
|
|
6
26
|
Every plugin config key, its file chain, and the cross-package config surface are inventoried in [../CONFIGURATION.md](../CONFIGURATION.md).
|
|
7
27
|
|
|
8
28
|
## Artifacts
|
|
@@ -37,7 +57,9 @@ itself):
|
|
|
37
57
|
opencode plugin add @mirrowel/opencode-openshare
|
|
38
58
|
```
|
|
39
59
|
|
|
40
|
-
Restart opencode; `/share` and `/openshare` appear in the TUI.
|
|
60
|
+
Restart opencode; `/share` and `/openshare` appear in the TUI. The
|
|
61
|
+
settings dialog's title carries the running plugin's version (read from
|
|
62
|
+
its own package manifest at runtime).
|
|
41
63
|
|
|
42
64
|
Then configure. For an npm install the durable config file is the
|
|
43
65
|
**settings home** (`~/.config/opencode/openshare.config.json` — also
|
|
@@ -149,6 +171,10 @@ queue, spool or wire:
|
|
|
149
171
|
value + source, full URLs/ids); Interface rows set menu width/height
|
|
150
172
|
(host preference store).
|
|
151
173
|
|
|
174
|
+
## Versioning
|
|
175
|
+
|
|
176
|
+
Releases are automatic (conventional commits). The plugin reports its version in the `/openshare` title; the system version ships in every part and the worker's `/health` carries it in full (`version · date · commit`). Designating a version by hand: `bash .github/scripts/release-kit.sh next <1.2.3|major|minor|patch>` in the repo (see DEVELOPMENT.md).
|
|
177
|
+
|
|
152
178
|
## Development
|
|
153
179
|
|
|
154
180
|
```bash
|
package/ci/openshare.js
CHANGED
|
@@ -2482,6 +2482,32 @@ function createEngine(deps) {
|
|
|
2482
2482
|
svelte: "svelte"
|
|
2483
2483
|
};
|
|
2484
2484
|
const FILE_TOOLS = new Set(["read", "write", "edit", "patch"]);
|
|
2485
|
+
const refineEmbeddedLanguage = (meta, text) => {
|
|
2486
|
+
if (meta.language !== "html")
|
|
2487
|
+
return false;
|
|
2488
|
+
const head = text.slice(0, 4000);
|
|
2489
|
+
if (/<[a-zA-Z][^>\n]*>/.test(head))
|
|
2490
|
+
return false;
|
|
2491
|
+
let refined;
|
|
2492
|
+
if (/^[ \t]*[.#][A-Za-z][\w-]*[^\n{]*\{/m.test(head) && !/\b(function|=>|return)\b/.test(head)) {
|
|
2493
|
+
refined = "css";
|
|
2494
|
+
} else if (/\b(function|const|let|var|return|if|for|while|=>|addEventListener|document\.|window\.)\b/.test(head) || /[;{}]\s*$/m.test(head)) {
|
|
2495
|
+
refined = "javascript";
|
|
2496
|
+
}
|
|
2497
|
+
if (!refined || meta.language === refined)
|
|
2498
|
+
return false;
|
|
2499
|
+
meta.language = refined;
|
|
2500
|
+
return true;
|
|
2501
|
+
};
|
|
2502
|
+
const refinePatchLanguage = (meta, patchText) => {
|
|
2503
|
+
const at = patchText.indexOf("@@");
|
|
2504
|
+
if (at < 0)
|
|
2505
|
+
return false;
|
|
2506
|
+
const body = patchText.slice(at).split(`
|
|
2507
|
+
`).map((l) => /^[ +\\]/.test(l) ? l.slice(1) : l.startsWith("-") ? l.slice(1) : "").join(`
|
|
2508
|
+
`);
|
|
2509
|
+
return refineEmbeddedLanguage(meta, body);
|
|
2510
|
+
};
|
|
2485
2511
|
const stampFileLanguages = () => {
|
|
2486
2512
|
for (const m of msgIndex.values()) {
|
|
2487
2513
|
if (m.role !== "assistant")
|
|
@@ -2508,6 +2534,15 @@ function createEngine(deps) {
|
|
|
2508
2534
|
mutated = true;
|
|
2509
2535
|
}
|
|
2510
2536
|
}
|
|
2537
|
+
if (part.name === "read" && typeof state.output === "string" && state.output.length > 0) {
|
|
2538
|
+
if (refineEmbeddedLanguage(meta, state.output))
|
|
2539
|
+
mutated = true;
|
|
2540
|
+
}
|
|
2541
|
+
if ((part.name === "edit" || part.name === "patch") && typeof meta.language === "string") {
|
|
2542
|
+
const pt = typeof meta.diff === "string" ? meta.diff : typeof meta.filediff?.patch === "string" ? meta.filediff.patch : undefined;
|
|
2543
|
+
if (pt && refinePatchLanguage(meta, pt))
|
|
2544
|
+
mutated = true;
|
|
2545
|
+
}
|
|
2511
2546
|
if (typeof meta.lineStart !== "number" && part.name === "read") {
|
|
2512
2547
|
const off = state.input?.offset;
|
|
2513
2548
|
if (typeof off === "number" && off >= 0 && off > 0) {
|
package/dist/config.d.ts
CHANGED
|
@@ -62,6 +62,9 @@ export declare const DEFAULTS: ShareConfig;
|
|
|
62
62
|
/** Directory of this module file (bundled standalone: the single file's
|
|
63
63
|
* directory; package: dist/). */
|
|
64
64
|
export declare function moduleDir(): string;
|
|
65
|
+
/** Version of the running plugin, from the package it was loaded from.
|
|
66
|
+
* "?" outside a package (single-file CI bundle with no sibling manifest). */
|
|
67
|
+
export declare function pluginVersion(): string;
|
|
65
68
|
/** Nearest ancestor (starting at `start`) containing package.json. */
|
|
66
69
|
export declare function packageRoot(start: string): string;
|
|
67
70
|
/** Config file search paths for a plugin living at `pluginDir`.
|
package/dist/config.js
CHANGED
|
@@ -51,6 +51,17 @@ const LOCAL_NAME = "openshare.local.json";
|
|
|
51
51
|
export function moduleDir() {
|
|
52
52
|
return dirname(fileURLToPath(import.meta.pathname ?? import.meta.url));
|
|
53
53
|
}
|
|
54
|
+
/** Version of the running plugin, from the package it was loaded from.
|
|
55
|
+
* "?" outside a package (single-file CI bundle with no sibling manifest). */
|
|
56
|
+
export function pluginVersion() {
|
|
57
|
+
try {
|
|
58
|
+
const pkg = JSON.parse(readFileSync(join(packageRoot(moduleDir()), "package.json"), "utf8"));
|
|
59
|
+
return pkg.version ?? "?";
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return "?";
|
|
63
|
+
}
|
|
64
|
+
}
|
|
54
65
|
/** Nearest ancestor (starting at `start`) containing package.json. */
|
|
55
66
|
export function packageRoot(start) {
|
|
56
67
|
let current = start;
|
package/dist/core.js
CHANGED
|
@@ -2209,6 +2209,44 @@ export function createEngine(deps) {
|
|
|
2209
2209
|
swift: "swift", kt: "kotlin", php: "php", lua: "lua", vue: "vue", svelte: "svelte",
|
|
2210
2210
|
};
|
|
2211
2211
|
const FILE_TOOLS = new Set(["read", "write", "edit", "patch"]);
|
|
2212
|
+
/** A read that returns lines from inside an html file's <script>/<style>
|
|
2213
|
+
* block gets tokenized as html by the viewer, and a snippet with no tag
|
|
2214
|
+
* context tokenizes as untyped soup (one default span per line). The
|
|
2215
|
+
* viewer only sees the snippet, so the stamp carries the language that is
|
|
2216
|
+
* true for the RETURNED RANGE: no tags plus code shape means the range sat
|
|
2217
|
+
* inside an embedded script or style block. */
|
|
2218
|
+
const refineEmbeddedLanguage = (meta, text) => {
|
|
2219
|
+
if (meta.language !== "html")
|
|
2220
|
+
return false;
|
|
2221
|
+
const head = text.slice(0, 4000);
|
|
2222
|
+
if (/<[a-zA-Z][^>\n]*>/.test(head))
|
|
2223
|
+
return false;
|
|
2224
|
+
let refined;
|
|
2225
|
+
if (/^[ \t]*[.#][A-Za-z][\w-]*[^\n{]*\{/m.test(head) && !/\b(function|=>|return)\b/.test(head)) {
|
|
2226
|
+
refined = "css";
|
|
2227
|
+
}
|
|
2228
|
+
else if (/\b(function|const|let|var|return|if|for|while|=>|addEventListener|document\.|window\.)\b/.test(head) || /[;{}]\s*$/m.test(head)) {
|
|
2229
|
+
refined = "javascript";
|
|
2230
|
+
}
|
|
2231
|
+
if (!refined || meta.language === refined)
|
|
2232
|
+
return false;
|
|
2233
|
+
meta.language = refined;
|
|
2234
|
+
return true;
|
|
2235
|
+
};
|
|
2236
|
+
/** Sniff the CODE CONTENT of a unified patch: drop everything before the
|
|
2237
|
+
* first hunk header and strip each line's diff marker, so the same
|
|
2238
|
+
* tag/shape checks as reads run over clean source lines. */
|
|
2239
|
+
const refinePatchLanguage = (meta, patchText) => {
|
|
2240
|
+
const at = patchText.indexOf("@@");
|
|
2241
|
+
if (at < 0)
|
|
2242
|
+
return false;
|
|
2243
|
+
const body = patchText
|
|
2244
|
+
.slice(at)
|
|
2245
|
+
.split("\n")
|
|
2246
|
+
.map((l) => (/^[ +\\]/.test(l) ? l.slice(1) : l.startsWith("-") ? l.slice(1) : ""))
|
|
2247
|
+
.join("\n");
|
|
2248
|
+
return refineEmbeddedLanguage(meta, body);
|
|
2249
|
+
};
|
|
2212
2250
|
const stampFileLanguages = () => {
|
|
2213
2251
|
for (const m of msgIndex.values()) {
|
|
2214
2252
|
if (m.role !== "assistant")
|
|
@@ -2240,6 +2278,18 @@ export function createEngine(deps) {
|
|
|
2240
2278
|
mutated = true;
|
|
2241
2279
|
}
|
|
2242
2280
|
}
|
|
2281
|
+
// range refinement: html-stamped reads inside script/style blocks
|
|
2282
|
+
if (part.name === "read" && typeof state.output === "string" && state.output.length > 0) {
|
|
2283
|
+
if (refineEmbeddedLanguage(meta, state.output))
|
|
2284
|
+
mutated = true;
|
|
2285
|
+
}
|
|
2286
|
+
// same refinement for edit hunks: a mid-script html diff tokenizes
|
|
2287
|
+
// untyped without the range language
|
|
2288
|
+
if ((part.name === "edit" || part.name === "patch") && typeof meta.language === "string") {
|
|
2289
|
+
const pt = typeof meta.diff === "string" ? meta.diff : typeof meta.filediff?.patch === "string" ? meta.filediff.patch : undefined;
|
|
2290
|
+
if (pt && refinePatchLanguage(meta, pt))
|
|
2291
|
+
mutated = true;
|
|
2292
|
+
}
|
|
2243
2293
|
// lineStart stamp: reads sliced from an offset keep REAL file line
|
|
2244
2294
|
// numbers in the viewer's unified file card (pierre numbers from 1;
|
|
2245
2295
|
// the viewer shifts by this stamp, authoritatively)
|
package/dist/settings.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// run on the v1 TUI and the v2 dialog bridge.
|
|
5
5
|
//
|
|
6
6
|
// Sections: Connection, Sharing, Read auth, Advanced, This session.
|
|
7
|
-
import { configSourceMap, moduleDir, writeCandidates } from "./config.js";
|
|
7
|
+
import { configSourceMap, moduleDir, pluginVersion, writeCandidates } from "./config.js";
|
|
8
8
|
import { adminClient, adminTokenFor, currentConfig, deleteServerEntry, disconnectAtServer, enrollAtServer, fmtHours, parseTtlHours, hostOf, isConfigured, normServerUrl, probeAuthMode, probeWorker, patchSessionBinding, patchShareOpts, readBinding, readBindings, resolveRoot, rotateAtServer, runUnshareFlow, saveConfig, saveServerEntry, meClient, serverEntries, setShareOpts, shareWithConsent, workerDeleteShare, } from "./share-flow.js";
|
|
9
9
|
import { DIALOG_WIDTH_COLUMNS, kvHeightPercentOf, kvWidthOf, nextDialogWidth, UI_HEIGHT_PERCENT_KV, UI_WIDTH_KV, clampHeightPercent } from "./ui-prefs.js";
|
|
10
10
|
/** description suffix: `[from <source>]` when a key is overridden by a
|
|
@@ -1452,7 +1452,7 @@ export async function settingsDialog(host, startAtSession, startGroup) {
|
|
|
1452
1452
|
help: group.help,
|
|
1453
1453
|
}));
|
|
1454
1454
|
options.push({ title: dirty ? "Save & exit" : "Exit", value: "action:exit", description: dirty ? `Write ${writeTarget.path}` : "No changes" });
|
|
1455
|
-
const choice = await showSelect(host.ui, { title:
|
|
1455
|
+
const choice = await showSelect(host.ui, { title: `OpenShare v${pluginVersion()} · settings`, options });
|
|
1456
1456
|
if (choice === undefined || choice === "action:exit") {
|
|
1457
1457
|
if (dirty) {
|
|
1458
1458
|
const saved = saveConfig(cfg, writeTarget.path);
|
package/dist/tui.js
CHANGED
|
@@ -36,6 +36,14 @@ var LOCAL_NAME = "openshare.local.json";
|
|
|
36
36
|
function moduleDir() {
|
|
37
37
|
return dirname(fileURLToPath(import.meta.pathname ?? import.meta.url));
|
|
38
38
|
}
|
|
39
|
+
function pluginVersion() {
|
|
40
|
+
try {
|
|
41
|
+
const pkg = JSON.parse(readFileSync(join(packageRoot(moduleDir()), "package.json"), "utf8"));
|
|
42
|
+
return pkg.version ?? "?";
|
|
43
|
+
} catch {
|
|
44
|
+
return "?";
|
|
45
|
+
}
|
|
46
|
+
}
|
|
39
47
|
function packageRoot(start) {
|
|
40
48
|
let current = start;
|
|
41
49
|
for (let guard = 0;guard < 16; guard++) {
|
|
@@ -2540,7 +2548,7 @@ Stored in the host's preference store \u2014 per machine, not the config file.`
|
|
|
2540
2548
|
help: group.help
|
|
2541
2549
|
}));
|
|
2542
2550
|
options.push({ title: dirty ? "Save & exit" : "Exit", value: "action:exit", description: dirty ? `Write ${writeTarget.path}` : "No changes" });
|
|
2543
|
-
const choice = await showSelect(host.ui, { title:
|
|
2551
|
+
const choice = await showSelect(host.ui, { title: `OpenShare v${pluginVersion()} \xB7 settings`, options });
|
|
2544
2552
|
if (choice === undefined || choice === "action:exit") {
|
|
2545
2553
|
if (dirty) {
|
|
2546
2554
|
const saved = saveConfig(cfg, writeTarget.path);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mirrowel/opencode-openshare",
|
|
3
|
-
"version": "0.3.0-dev.
|
|
3
|
+
"version": "0.3.0-dev.10",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Open(code)Share — re:share: self-hosted session sharing for opencode. Server plugin (single file or package), optional TUI for interactive opt-in sharing.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -64,10 +64,8 @@
|
|
|
64
64
|
"build:tui": "bun scripts/build-tui.mjs",
|
|
65
65
|
"build:standalone": "bun scripts/build-standalone.mjs",
|
|
66
66
|
"typecheck": "tsc --noEmit",
|
|
67
|
-
"ci": "npm run
|
|
67
|
+
"ci": "npm run ci:package",
|
|
68
68
|
"ci:package": "npm run typecheck && npm run build && npm run pack:dry-run",
|
|
69
|
-
"release:intent": "node scripts/set-release-intent.mjs",
|
|
70
|
-
"release:intent:check": "node scripts/check-release-intent.mjs",
|
|
71
69
|
"pack:dry-run": "node scripts/pack-dry-run.mjs",
|
|
72
70
|
"test:regression": "node scripts/regression-tests.mjs",
|
|
73
71
|
"prepublishOnly": "npm run typecheck && npm run build",
|
package/src/config.ts
CHANGED
|
@@ -115,9 +115,19 @@ export function moduleDir(): string {
|
|
|
115
115
|
return dirname(fileURLToPath((import.meta as { pathname?: string }).pathname ?? import.meta.url))
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
/** Version of the running plugin, from the package it was loaded from.
|
|
119
|
+
* "?" outside a package (single-file CI bundle with no sibling manifest). */
|
|
120
|
+
export function pluginVersion(): string {
|
|
121
|
+
try {
|
|
122
|
+
const pkg = JSON.parse(readFileSync(join(packageRoot(moduleDir()), "package.json"), "utf8")) as { version?: string }
|
|
123
|
+
return pkg.version ?? "?"
|
|
124
|
+
} catch {
|
|
125
|
+
return "?"
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
118
129
|
/** Nearest ancestor (starting at `start`) containing package.json. */
|
|
119
|
-
export function packageRoot(start: string): string {
|
|
120
|
-
let current = start
|
|
130
|
+
export function packageRoot(start: string): string { let current = start
|
|
121
131
|
for (let guard = 0; guard < 16; guard++) {
|
|
122
132
|
if (existsSync(join(current, "package.json"))) return current
|
|
123
133
|
const parent = dirname(current)
|
package/src/core.ts
CHANGED
|
@@ -2263,6 +2263,41 @@ export function createEngine(deps: EngineDeps): Engine {
|
|
|
2263
2263
|
|
|
2264
2264
|
const FILE_TOOLS = new Set(["read", "write", "edit", "patch"])
|
|
2265
2265
|
|
|
2266
|
+
/** A read that returns lines from inside an html file's <script>/<style>
|
|
2267
|
+
* block gets tokenized as html by the viewer, and a snippet with no tag
|
|
2268
|
+
* context tokenizes as untyped soup (one default span per line). The
|
|
2269
|
+
* viewer only sees the snippet, so the stamp carries the language that is
|
|
2270
|
+
* true for the RETURNED RANGE: no tags plus code shape means the range sat
|
|
2271
|
+
* inside an embedded script or style block. */
|
|
2272
|
+
const refineEmbeddedLanguage = (meta: { language?: unknown }, text: string): boolean => {
|
|
2273
|
+
if (meta.language !== "html") return false
|
|
2274
|
+
const head = text.slice(0, 4000)
|
|
2275
|
+
if (/<[a-zA-Z][^>\n]*>/.test(head)) return false
|
|
2276
|
+
let refined: string | undefined
|
|
2277
|
+
if (/^[ \t]*[.#][A-Za-z][\w-]*[^\n{]*\{/m.test(head) && !/\b(function|=>|return)\b/.test(head)) {
|
|
2278
|
+
refined = "css"
|
|
2279
|
+
} else if (/\b(function|const|let|var|return|if|for|while|=>|addEventListener|document\.|window\.)\b/.test(head) || /[;{}]\s*$/m.test(head)) {
|
|
2280
|
+
refined = "javascript"
|
|
2281
|
+
}
|
|
2282
|
+
if (!refined || meta.language === refined) return false
|
|
2283
|
+
meta.language = refined
|
|
2284
|
+
return true
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
/** Sniff the CODE CONTENT of a unified patch: drop everything before the
|
|
2288
|
+
* first hunk header and strip each line's diff marker, so the same
|
|
2289
|
+
* tag/shape checks as reads run over clean source lines. */
|
|
2290
|
+
const refinePatchLanguage = (meta: { language?: unknown }, patchText: string): boolean => {
|
|
2291
|
+
const at = patchText.indexOf("@@")
|
|
2292
|
+
if (at < 0) return false
|
|
2293
|
+
const body = patchText
|
|
2294
|
+
.slice(at)
|
|
2295
|
+
.split("\n")
|
|
2296
|
+
.map((l) => (/^[ +\\]/.test(l) ? l.slice(1) : l.startsWith("-") ? l.slice(1) : ""))
|
|
2297
|
+
.join("\n")
|
|
2298
|
+
return refineEmbeddedLanguage(meta, body)
|
|
2299
|
+
}
|
|
2300
|
+
|
|
2266
2301
|
const stampFileLanguages = () => {
|
|
2267
2302
|
for (const m of msgIndex.values()) {
|
|
2268
2303
|
if (m.role !== "assistant") continue
|
|
@@ -2289,6 +2324,16 @@ export function createEngine(deps: EngineDeps): Engine {
|
|
|
2289
2324
|
mutated = true
|
|
2290
2325
|
}
|
|
2291
2326
|
}
|
|
2327
|
+
// range refinement: html-stamped reads inside script/style blocks
|
|
2328
|
+
if (part.name === "read" && typeof state.output === "string" && state.output.length > 0) {
|
|
2329
|
+
if (refineEmbeddedLanguage(meta, state.output)) mutated = true
|
|
2330
|
+
}
|
|
2331
|
+
// same refinement for edit hunks: a mid-script html diff tokenizes
|
|
2332
|
+
// untyped without the range language
|
|
2333
|
+
if ((part.name === "edit" || part.name === "patch") && typeof meta.language === "string") {
|
|
2334
|
+
const pt = typeof (meta as any).diff === "string" ? (meta as any).diff : typeof (meta as any).filediff?.patch === "string" ? (meta as any).filediff.patch : undefined
|
|
2335
|
+
if (pt && refinePatchLanguage(meta, pt)) mutated = true
|
|
2336
|
+
}
|
|
2292
2337
|
// lineStart stamp: reads sliced from an offset keep REAL file line
|
|
2293
2338
|
// numbers in the viewer's unified file card (pierre numbers from 1;
|
|
2294
2339
|
// the viewer shifts by this stamp, authoritatively)
|
package/src/settings.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
//
|
|
6
6
|
// Sections: Connection, Sharing, Read auth, Advanced, This session.
|
|
7
7
|
|
|
8
|
-
import { configSourceMap, moduleDir, writeCandidates, type ShareConfig, type WriteCandidate } from "./config.js"
|
|
8
|
+
import { configSourceMap, moduleDir, pluginVersion, writeCandidates, type ShareConfig, type WriteCandidate } from "./config.js"
|
|
9
9
|
import {
|
|
10
10
|
adminClient,
|
|
11
11
|
adminTokenFor,
|
|
@@ -1462,7 +1462,7 @@ export async function settingsDialog(host: ShareHostApi, startAtSession?: string
|
|
|
1462
1462
|
}))
|
|
1463
1463
|
options.push({ title: dirty ? "Save & exit" : "Exit", value: "action:exit", description: dirty ? `Write ${writeTarget.path}` : "No changes" })
|
|
1464
1464
|
|
|
1465
|
-
const choice = await showSelect(host.ui, { title:
|
|
1465
|
+
const choice = await showSelect(host.ui, { title: `OpenShare v${pluginVersion()} · settings`, options })
|
|
1466
1466
|
if (choice === undefined || choice === "action:exit") {
|
|
1467
1467
|
if (dirty) {
|
|
1468
1468
|
const saved = saveConfig(cfg, writeTarget.path)
|