@jmfederico/pi-web 1.202607.2 → 1.202607.3
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/dist/client/assets/{CodeViewer-B4M13447.js → CodeViewer-CDVbMiN9.js} +1 -1
- package/dist/client/assets/{TerminalPanel-Bmvj-Ecn.js → TerminalPanel-CTS1CgqF.js} +2 -2
- package/dist/client/assets/{UnifiedDiffViewer-C0hygwRo.js → UnifiedDiffViewer-Cqke12ZX.js} +1 -1
- package/dist/client/assets/index-LME0LfPb.js +4068 -0
- package/dist/client/index.html +1 -1
- package/dist/config.js +39 -0
- package/dist/config.js.map +1 -1
- package/dist/pi-web-plugins/info/infoInternals.js +208 -0
- package/dist/pi-web-plugins/info/pi-web-plugin.js +12 -15
- package/dist/pi-web-plugins/relays/markdownDocument.js +81 -0
- package/dist/pi-web-plugins/relays/package.json +9 -0
- package/dist/pi-web-plugins/relays/pi-web-plugin.js +44 -0
- package/dist/pi-web-plugins/relays/relayDiscovery.js +96 -0
- package/dist/pi-web-plugins/relays/relaysPanelElement.js +393 -0
- package/dist/pi-web-plugins/relays/vendor/README.md +24 -0
- package/dist/pi-web-plugins/relays/vendor/marked.esm.js +76 -0
- package/dist/plugin-api.d.ts +7 -1
- package/dist/server/configRoutes.js +11 -0
- package/dist/server/configRoutes.js.map +1 -1
- package/dist/server/sessiond/sessionServiceDependencies.js +32 -0
- package/dist/server/sessiond/sessionServiceDependencies.js.map +1 -0
- package/dist/server/sessiond.js +122 -114
- package/dist/server/sessiond.js.map +1 -1
- package/dist/server/sessions/askUserTool.js +105 -0
- package/dist/server/sessions/askUserTool.js.map +1 -0
- package/dist/server/sessions/extensionDialogWaiters.js +76 -0
- package/dist/server/sessions/extensionDialogWaiters.js.map +1 -0
- package/dist/server/sessions/globalProviderPolicy.js +79 -10
- package/dist/server/sessions/globalProviderPolicy.js.map +1 -1
- package/dist/server/sessions/modelCatalogRefresher.js +8 -0
- package/dist/server/sessions/modelCatalogRefresher.js.map +1 -1
- package/dist/server/sessions/parentSessionLocator.js +75 -0
- package/dist/server/sessions/parentSessionLocator.js.map +1 -0
- package/dist/server/sessions/pendingAskStore.js +278 -0
- package/dist/server/sessions/pendingAskStore.js.map +1 -0
- package/dist/server/sessions/pendingExtensionDialogStore.js +196 -0
- package/dist/server/sessions/pendingExtensionDialogStore.js.map +1 -0
- package/dist/server/sessions/piSessionService.js +504 -44
- package/dist/server/sessions/piSessionService.js.map +1 -1
- package/dist/server/sessions/sessionFileHeader.js +45 -0
- package/dist/server/sessions/sessionFileHeader.js.map +1 -0
- package/dist/server/sessions/sessionRoutes.js +99 -2
- package/dist/server/sessions/sessionRoutes.js.map +1 -1
- package/dist/server/sessions/spawnTargetResolver.js +3 -16
- package/dist/server/sessions/spawnTargetResolver.js.map +1 -1
- package/dist/server/workspaces/gitWorktreeDiscovery.js +9 -0
- package/dist/server/workspaces/gitWorktreeDiscovery.js.map +1 -1
- package/dist/server/workspaces/projectWorkspaceCwds.js +22 -0
- package/dist/server/workspaces/projectWorkspaceCwds.js.map +1 -0
- package/dist/server/workspaces/workspaceService.js +15 -2
- package/dist/server/workspaces/workspaceService.js.map +1 -1
- package/dist/shared/activity.js +9 -1
- package/dist/shared/activity.js.map +1 -1
- package/dist/shared/apiTypes.d.ts +294 -1
- package/dist/shared/apiTypes.js +24 -0
- package/dist/shared/apiTypes.js.map +1 -1
- package/dist/shared/capabilities.js +3 -0
- package/dist/shared/capabilities.js.map +1 -1
- package/dist/shared/federatedRoutes.js +4 -0
- package/dist/shared/federatedRoutes.js.map +1 -1
- package/docs/config.md +56 -7
- package/docs/plugins.md +77 -11
- package/package.json +2 -1
- package/dist/client/assets/index-TvPsRyS1.js +0 -3545
- package/dist/server/sessiond/sessionDaemonStartup.js +0 -49
- package/dist/server/sessiond/sessionDaemonStartup.js.map +0 -1
- package/dist/server/sessions/sessionArchiveMigration.js +0 -590
- package/dist/server/sessions/sessionArchiveMigration.js.map +0 -1
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Generated from pi-web-plugins/relays/relayDiscovery.ts. Do not edit directly.
|
|
2
|
+
export const RELAYS_ROOT = ".pi-web/relays";
|
|
3
|
+
/** Documents that anchor a relay packet, in display order. Any other files follow alphabetically. */
|
|
4
|
+
export const RELAY_ANCHOR_DOCUMENTS = ["status.md", "charter.md", "log.md"];
|
|
5
|
+
// The workspace file API rejects with these messages when a path is absent or
|
|
6
|
+
// is not a directory. For the relays root both mean "zero relays", not a failure.
|
|
7
|
+
const missingListingErrorMessages = new Set(["Path does not exist", "Path is not a directory"]);
|
|
8
|
+
/** List the workspace's relays, most recently modified first. Never rejects. */
|
|
9
|
+
export async function listWorkspaceRelays(files) {
|
|
10
|
+
let listing;
|
|
11
|
+
try {
|
|
12
|
+
listing = await files.listFiles(RELAYS_ROOT);
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
return fileAccessFailure(error);
|
|
16
|
+
}
|
|
17
|
+
const relays = sortRelaysByRecency(listing.entries
|
|
18
|
+
.filter((entry) => entry.type === "directory")
|
|
19
|
+
.map((entry) => toRelaySummary(entry)));
|
|
20
|
+
return { kind: "loaded", relays };
|
|
21
|
+
}
|
|
22
|
+
/** List one relay's documents: status.md, charter.md, log.md first, then alphabetical. Never rejects. */
|
|
23
|
+
export async function listRelayDocuments(files, relayPath) {
|
|
24
|
+
let listing;
|
|
25
|
+
try {
|
|
26
|
+
listing = await files.listFiles(relayPath);
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
return fileAccessFailure(error);
|
|
30
|
+
}
|
|
31
|
+
const documents = orderRelayDocuments(listing.entries
|
|
32
|
+
.filter((entry) => entry.type === "file")
|
|
33
|
+
.map((entry) => toRelayDocumentEntry(entry)));
|
|
34
|
+
return { kind: "loaded", documents };
|
|
35
|
+
}
|
|
36
|
+
/** Read one relay document. Never rejects. */
|
|
37
|
+
export async function readRelayDocument(files, documentPath) {
|
|
38
|
+
try {
|
|
39
|
+
const file = await files.readFile(documentPath);
|
|
40
|
+
return { kind: "loaded", content: file.content, truncated: file.truncated, binary: file.binary };
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
return fileAccessFailure(error);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** Newest first; relays without a usable modifiedAt sort last, alphabetically. */
|
|
47
|
+
export function sortRelaysByRecency(relays) {
|
|
48
|
+
return [...relays].sort(compareRelaysByRecency);
|
|
49
|
+
}
|
|
50
|
+
/** Anchor documents first in fixed order, then everything else alphabetically. */
|
|
51
|
+
export function orderRelayDocuments(documents) {
|
|
52
|
+
return [...documents].sort(compareRelayDocuments);
|
|
53
|
+
}
|
|
54
|
+
/** The document a relay opens on: status.md when present, otherwise the first ordered document. */
|
|
55
|
+
export function defaultRelayDocument(documents) {
|
|
56
|
+
// Anchor ordering guarantees status.md is first whenever it exists.
|
|
57
|
+
return orderRelayDocuments(documents)[0];
|
|
58
|
+
}
|
|
59
|
+
function toRelaySummary(entry) {
|
|
60
|
+
return { name: entry.name, path: entry.path, modifiedAt: entry.modifiedAt };
|
|
61
|
+
}
|
|
62
|
+
function toRelayDocumentEntry(entry) {
|
|
63
|
+
return { name: entry.name, path: entry.path, modifiedAt: entry.modifiedAt };
|
|
64
|
+
}
|
|
65
|
+
function compareRelaysByRecency(left, right) {
|
|
66
|
+
const leftTime = timestampOf(left.modifiedAt);
|
|
67
|
+
const rightTime = timestampOf(right.modifiedAt);
|
|
68
|
+
if (leftTime !== undefined && rightTime !== undefined && leftTime !== rightTime)
|
|
69
|
+
return rightTime - leftTime;
|
|
70
|
+
if (leftTime !== undefined)
|
|
71
|
+
return -1;
|
|
72
|
+
if (rightTime !== undefined)
|
|
73
|
+
return 1;
|
|
74
|
+
return left.name.localeCompare(right.name);
|
|
75
|
+
}
|
|
76
|
+
function timestampOf(modifiedAt) {
|
|
77
|
+
if (modifiedAt === undefined)
|
|
78
|
+
return undefined;
|
|
79
|
+
const time = Date.parse(modifiedAt);
|
|
80
|
+
return Number.isNaN(time) ? undefined : time;
|
|
81
|
+
}
|
|
82
|
+
function compareRelayDocuments(left, right) {
|
|
83
|
+
const anchorOrder = anchorIndexOf(left.name) - anchorIndexOf(right.name);
|
|
84
|
+
if (anchorOrder !== 0)
|
|
85
|
+
return anchorOrder;
|
|
86
|
+
return left.name.localeCompare(right.name);
|
|
87
|
+
}
|
|
88
|
+
function anchorIndexOf(name) {
|
|
89
|
+
const index = RELAY_ANCHOR_DOCUMENTS.indexOf(name);
|
|
90
|
+
return index === -1 ? RELAY_ANCHOR_DOCUMENTS.length : index;
|
|
91
|
+
}
|
|
92
|
+
function fileAccessFailure(error) {
|
|
93
|
+
if (error instanceof Error && missingListingErrorMessages.has(error.message))
|
|
94
|
+
return { kind: "missing" };
|
|
95
|
+
return { kind: "unavailable", detail: error instanceof Error ? error.message : String(error) };
|
|
96
|
+
}
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
// Generated from pi-web-plugins/relays/relaysPanelElement.ts. Do not edit directly.
|
|
2
|
+
import { isMarkdownDocumentPath, renderRelayDocumentHtml } from "./markdownDocument.js";
|
|
3
|
+
import { defaultRelayDocument, listRelayDocuments, listWorkspaceRelays, readRelayDocument, RELAYS_ROOT, } from "./relayDiscovery.js";
|
|
4
|
+
export const relaysPanelTagName = "pi-web-relays-panel";
|
|
5
|
+
export function defineRelaysPanelElement() {
|
|
6
|
+
if (!customElements.get(relaysPanelTagName))
|
|
7
|
+
customElements.define(relaysPanelTagName, PiWebRelaysPanel);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Read-only relay browser: relay picker (auto-opens a single relay), one tab
|
|
11
|
+
* per relay document, and a document viewer rendering markdown documents as
|
|
12
|
+
* sanitized HTML and everything else as preformatted text. All async loads
|
|
13
|
+
* flow through scanToken so stale responses for a previous workspace or
|
|
14
|
+
* selection never overwrite newer state.
|
|
15
|
+
*
|
|
16
|
+
* Rendering is region-scoped: the toolbar, tab strip, and viewer are
|
|
17
|
+
* persistent elements built once, and each async stage re-renders only its
|
|
18
|
+
* own region. Clicking a tab never rebuilds the strip — it toggles the
|
|
19
|
+
* active marker in place and re-renders the viewer — so the strip's
|
|
20
|
+
* horizontal scroll position and keyboard focus survive document switches.
|
|
21
|
+
*/
|
|
22
|
+
class PiWebRelaysPanel extends HTMLElement {
|
|
23
|
+
contextValue;
|
|
24
|
+
listing;
|
|
25
|
+
selectedRelayPath;
|
|
26
|
+
documents;
|
|
27
|
+
selectedDocumentPath;
|
|
28
|
+
documentContent;
|
|
29
|
+
scanToken = 0;
|
|
30
|
+
root;
|
|
31
|
+
toolbar;
|
|
32
|
+
tabStrip;
|
|
33
|
+
viewer;
|
|
34
|
+
constructor() {
|
|
35
|
+
super();
|
|
36
|
+
this.root = this.attachShadow({ mode: "open" });
|
|
37
|
+
this.root.innerHTML = `
|
|
38
|
+
${relaysStyles()}
|
|
39
|
+
<section class="toolbar" hidden></section>
|
|
40
|
+
<nav class="document-tabs" aria-label="Relay documents" hidden></nav>
|
|
41
|
+
<section class="viewer"><div class="empty">Select a workspace.</div></section>
|
|
42
|
+
`;
|
|
43
|
+
this.toolbar = requiredRegion(this.root, ".toolbar");
|
|
44
|
+
this.tabStrip = requiredRegion(this.root, "nav.document-tabs");
|
|
45
|
+
this.viewer = requiredRegion(this.root, ".viewer");
|
|
46
|
+
// Listeners bind once against the persistent regions and delegate to
|
|
47
|
+
// whichever controls the latest region render produced.
|
|
48
|
+
this.toolbar.addEventListener("click", (event) => {
|
|
49
|
+
const button = event.target instanceof Element ? event.target.closest("button[data-refresh]") : null;
|
|
50
|
+
if (button !== null)
|
|
51
|
+
this.refresh();
|
|
52
|
+
});
|
|
53
|
+
this.toolbar.addEventListener("change", (event) => {
|
|
54
|
+
const picker = event.target;
|
|
55
|
+
if (!(picker instanceof HTMLSelectElement) || !picker.matches("select[data-relay-picker]"))
|
|
56
|
+
return;
|
|
57
|
+
const context = this.contextValue;
|
|
58
|
+
if (context !== undefined)
|
|
59
|
+
void this.openRelay(context, picker.value);
|
|
60
|
+
});
|
|
61
|
+
this.tabStrip.addEventListener("click", (event) => {
|
|
62
|
+
const tab = event.target instanceof Element ? event.target.closest("button[data-document-path]") : null;
|
|
63
|
+
if (tab === null)
|
|
64
|
+
return;
|
|
65
|
+
const documentPath = tab.getAttribute("data-document-path");
|
|
66
|
+
const context = this.contextValue;
|
|
67
|
+
if (context !== undefined && documentPath !== null)
|
|
68
|
+
void this.openDocument(context, documentPath);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
set context(value) {
|
|
72
|
+
const previousKey = this.contextValue === undefined ? undefined : contextKey(this.contextValue);
|
|
73
|
+
const nextKey = value === undefined ? undefined : contextKey(value);
|
|
74
|
+
this.contextValue = value;
|
|
75
|
+
// Parent app updates should not rescan or re-render this panel for the
|
|
76
|
+
// same workspace (mirrors the workspace-tasks panel).
|
|
77
|
+
if (previousKey === nextKey)
|
|
78
|
+
return;
|
|
79
|
+
if (value === undefined) {
|
|
80
|
+
this.resetScanState();
|
|
81
|
+
this.renderAll();
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
void this.scan(value, {});
|
|
85
|
+
}
|
|
86
|
+
/** Rescan relays, then reload the selected relay's documents and the open document. */
|
|
87
|
+
async scan(context, selection) {
|
|
88
|
+
const token = ++this.scanToken;
|
|
89
|
+
this.resetScanState();
|
|
90
|
+
this.renderAll();
|
|
91
|
+
const listing = await listWorkspaceRelays(context.files);
|
|
92
|
+
if (!this.isCurrentScan(context, token))
|
|
93
|
+
return;
|
|
94
|
+
this.listing = listing;
|
|
95
|
+
// listWorkspaceRelays returns most recently modified first, so the first
|
|
96
|
+
// relay is the default pre-selection.
|
|
97
|
+
const relay = listing.kind === "loaded"
|
|
98
|
+
? listing.relays.find((candidate) => candidate.path === selection.relayPath) ?? listing.relays[0]
|
|
99
|
+
: undefined;
|
|
100
|
+
this.selectedRelayPath = relay?.path;
|
|
101
|
+
this.renderToolbar();
|
|
102
|
+
if (relay === undefined) {
|
|
103
|
+
this.renderViewer();
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
await this.loadDocuments(context, token, relay.path, selection.documentPath);
|
|
107
|
+
}
|
|
108
|
+
async openRelay(context, relayPath) {
|
|
109
|
+
const token = ++this.scanToken;
|
|
110
|
+
this.selectedRelayPath = relayPath;
|
|
111
|
+
await this.loadDocuments(context, token, relayPath, undefined);
|
|
112
|
+
}
|
|
113
|
+
async openDocument(context, documentPath) {
|
|
114
|
+
const token = ++this.scanToken;
|
|
115
|
+
this.selectedDocumentPath = documentPath;
|
|
116
|
+
// The tab set is unchanged: toggle the active marker on the mounted
|
|
117
|
+
// buttons instead of rebuilding the strip, so its scroll position and
|
|
118
|
+
// focus stay put. A different document starts reading from the top.
|
|
119
|
+
this.updateActiveTab();
|
|
120
|
+
this.viewer.scrollTop = 0;
|
|
121
|
+
await this.loadDocumentContent(context, token, documentPath);
|
|
122
|
+
}
|
|
123
|
+
async loadDocuments(context, token, relayPath, preferredDocumentPath) {
|
|
124
|
+
this.documents = undefined;
|
|
125
|
+
this.selectedDocumentPath = undefined;
|
|
126
|
+
this.documentContent = undefined;
|
|
127
|
+
this.renderTabs();
|
|
128
|
+
this.renderViewer();
|
|
129
|
+
const documents = await listRelayDocuments(context.files, relayPath);
|
|
130
|
+
if (!this.isCurrentScan(context, token))
|
|
131
|
+
return;
|
|
132
|
+
this.documents = documents;
|
|
133
|
+
const document = documents.kind === "loaded"
|
|
134
|
+
? documents.documents.find((candidate) => candidate.path === preferredDocumentPath) ?? defaultRelayDocument(documents.documents)
|
|
135
|
+
: undefined;
|
|
136
|
+
this.selectedDocumentPath = document?.path;
|
|
137
|
+
this.renderTabs();
|
|
138
|
+
if (document === undefined) {
|
|
139
|
+
this.renderViewer();
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
await this.loadDocumentContent(context, token, document.path);
|
|
143
|
+
}
|
|
144
|
+
async loadDocumentContent(context, token, documentPath) {
|
|
145
|
+
this.documentContent = undefined;
|
|
146
|
+
this.renderViewer();
|
|
147
|
+
const content = await readRelayDocument(context.files, documentPath);
|
|
148
|
+
if (!this.isCurrentScan(context, token))
|
|
149
|
+
return;
|
|
150
|
+
this.documentContent = content;
|
|
151
|
+
this.renderViewer();
|
|
152
|
+
}
|
|
153
|
+
refresh() {
|
|
154
|
+
const context = this.contextValue;
|
|
155
|
+
if (context === undefined)
|
|
156
|
+
return;
|
|
157
|
+
void this.scan(context, { relayPath: this.selectedRelayPath, documentPath: this.selectedDocumentPath });
|
|
158
|
+
}
|
|
159
|
+
resetScanState() {
|
|
160
|
+
this.listing = undefined;
|
|
161
|
+
this.selectedRelayPath = undefined;
|
|
162
|
+
this.documents = undefined;
|
|
163
|
+
this.selectedDocumentPath = undefined;
|
|
164
|
+
this.documentContent = undefined;
|
|
165
|
+
}
|
|
166
|
+
isCurrentScan(context, token) {
|
|
167
|
+
return token === this.scanToken && this.contextValue !== undefined && contextKey(this.contextValue) === contextKey(context);
|
|
168
|
+
}
|
|
169
|
+
renderAll() {
|
|
170
|
+
this.renderToolbar();
|
|
171
|
+
this.renderTabs();
|
|
172
|
+
this.renderViewer();
|
|
173
|
+
}
|
|
174
|
+
renderToolbar() {
|
|
175
|
+
if (this.contextValue === undefined) {
|
|
176
|
+
this.toolbar.hidden = true;
|
|
177
|
+
this.toolbar.replaceChildren();
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
this.toolbar.hidden = false;
|
|
181
|
+
this.toolbar.innerHTML = `
|
|
182
|
+
<strong>Relays</strong>
|
|
183
|
+
<span class="toolbar-actions">
|
|
184
|
+
${this.renderRelayPicker()}
|
|
185
|
+
<button class="icon-button" data-refresh aria-label="Refresh" title="Refresh">${refreshIconSvg()}</button>
|
|
186
|
+
</span>
|
|
187
|
+
`;
|
|
188
|
+
}
|
|
189
|
+
renderRelayPicker() {
|
|
190
|
+
const listing = this.listing;
|
|
191
|
+
if (listing?.kind !== "loaded" || listing.relays.length === 0)
|
|
192
|
+
return "";
|
|
193
|
+
// A single relay opens immediately; a one-option picker would be noise.
|
|
194
|
+
if (listing.relays.length === 1) {
|
|
195
|
+
const relay = listing.relays[0];
|
|
196
|
+
return relay === undefined ? "" : `<span class="relay-name" title="${escapeAttr(relay.path)}">${escapeHtml(relay.name)}</span>`;
|
|
197
|
+
}
|
|
198
|
+
const options = listing.relays.map((relay) => {
|
|
199
|
+
const selected = relay.path === this.selectedRelayPath ? " selected" : "";
|
|
200
|
+
return `<option value="${escapeAttr(relay.path)}"${selected}>${escapeHtml(relay.name)}</option>`;
|
|
201
|
+
}).join("");
|
|
202
|
+
return `<select data-relay-picker aria-label="Relay">${options}</select>`;
|
|
203
|
+
}
|
|
204
|
+
renderTabs() {
|
|
205
|
+
const documents = this.documents;
|
|
206
|
+
if (documents?.kind !== "loaded" || documents.documents.length === 0) {
|
|
207
|
+
this.tabStrip.hidden = true;
|
|
208
|
+
// A new tab set starts at the left edge, not at the previous set's offset.
|
|
209
|
+
this.tabStrip.replaceChildren();
|
|
210
|
+
this.tabStrip.scrollLeft = 0;
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
this.tabStrip.hidden = false;
|
|
214
|
+
// The strip element itself persists across re-renders, so replacing its
|
|
215
|
+
// buttons keeps the container's horizontal scroll position.
|
|
216
|
+
this.tabStrip.innerHTML = documents.documents.map((document) => {
|
|
217
|
+
const active = document.path === this.selectedDocumentPath;
|
|
218
|
+
return `<button class="document-tab${active ? " active" : ""}" data-document-path="${escapeAttr(document.path)}"${active ? ' aria-current="true"' : ""}>${escapeHtml(document.name)}</button>`;
|
|
219
|
+
}).join("");
|
|
220
|
+
}
|
|
221
|
+
/** Move the active marker between the mounted tab buttons without rebuilding them. */
|
|
222
|
+
updateActiveTab() {
|
|
223
|
+
for (const tab of this.tabStrip.querySelectorAll("button[data-document-path]")) {
|
|
224
|
+
const active = tab.getAttribute("data-document-path") === this.selectedDocumentPath;
|
|
225
|
+
tab.classList.toggle("active", active);
|
|
226
|
+
if (active)
|
|
227
|
+
tab.setAttribute("aria-current", "true");
|
|
228
|
+
else
|
|
229
|
+
tab.removeAttribute("aria-current");
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
renderViewer() {
|
|
233
|
+
if (this.contextValue === undefined) {
|
|
234
|
+
this.viewer.innerHTML = `<div class="empty">Select a workspace.</div>`;
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
this.viewer.innerHTML = this.renderViewerContent();
|
|
238
|
+
}
|
|
239
|
+
renderViewerContent() {
|
|
240
|
+
const listing = this.listing;
|
|
241
|
+
if (listing === undefined)
|
|
242
|
+
return `<p class="muted">Scanning ${escapeHtml(RELAYS_ROOT)}…</p>`;
|
|
243
|
+
if (listing.kind === "unavailable")
|
|
244
|
+
return renderErrorState("Could not scan workspace relays.", listing.detail);
|
|
245
|
+
if (listing.kind === "missing" || listing.relays.length === 0)
|
|
246
|
+
return renderEmptyState();
|
|
247
|
+
return this.renderSelectedRelay();
|
|
248
|
+
}
|
|
249
|
+
renderSelectedRelay() {
|
|
250
|
+
const documents = this.documents;
|
|
251
|
+
if (documents === undefined)
|
|
252
|
+
return `<p class="muted">Loading relay documents…</p>`;
|
|
253
|
+
if (documents.kind === "unavailable")
|
|
254
|
+
return renderErrorState("Could not list this relay's documents.", documents.detail);
|
|
255
|
+
if (documents.kind === "missing") {
|
|
256
|
+
return `<div class="empty-state"><strong>This relay no longer exists.</strong><p>Click Refresh to rescan ${escapeHtml(RELAYS_ROOT)}.</p></div>`;
|
|
257
|
+
}
|
|
258
|
+
if (documents.documents.length === 0) {
|
|
259
|
+
return `
|
|
260
|
+
<div class="empty-state">
|
|
261
|
+
<strong>This relay has no documents yet.</strong>
|
|
262
|
+
<p>Relay packets usually contain <code>status.md</code>, <code>charter.md</code>, and <code>log.md</code>.</p>
|
|
263
|
+
</div>
|
|
264
|
+
`;
|
|
265
|
+
}
|
|
266
|
+
return this.renderSelectedDocument();
|
|
267
|
+
}
|
|
268
|
+
renderSelectedDocument() {
|
|
269
|
+
const documentPath = this.selectedDocumentPath;
|
|
270
|
+
const content = this.documentContent;
|
|
271
|
+
if (documentPath === undefined)
|
|
272
|
+
return `<p class="muted">Select a document.</p>`;
|
|
273
|
+
if (content === undefined)
|
|
274
|
+
return `<p class="muted">Loading ${escapeHtml(documentName(documentPath))}…</p>`;
|
|
275
|
+
if (content.kind === "unavailable")
|
|
276
|
+
return renderErrorState("Could not read this document.", content.detail);
|
|
277
|
+
if (content.kind === "missing") {
|
|
278
|
+
return `<div class="empty-state"><strong>This document no longer exists.</strong><p>Click Refresh to rescan the relay.</p></div>`;
|
|
279
|
+
}
|
|
280
|
+
if (content.binary) {
|
|
281
|
+
return `<div class="empty-state"><strong>Binary file: ${escapeHtml(documentName(documentPath))}</strong><p>Binary documents have no text preview.</p></div>`;
|
|
282
|
+
}
|
|
283
|
+
const truncation = content.truncated
|
|
284
|
+
? `<div class="status info">This document is truncated — only the beginning is shown.</div>`
|
|
285
|
+
: "";
|
|
286
|
+
if (isMarkdownDocumentPath(documentPath)) {
|
|
287
|
+
return `${truncation}<div class="document markdown">${renderRelayDocumentHtml(content.content)}</div>`;
|
|
288
|
+
}
|
|
289
|
+
return `${truncation}<pre class="document">${escapeHtml(content.content)}</pre>`;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
/** Shell regions come from a literal template; absence means the template broke. */
|
|
293
|
+
function requiredRegion(root, selector) {
|
|
294
|
+
const element = root.querySelector(selector);
|
|
295
|
+
if (!(element instanceof HTMLElement))
|
|
296
|
+
throw new Error(`relays panel shell is missing ${selector}`);
|
|
297
|
+
return element;
|
|
298
|
+
}
|
|
299
|
+
/** Reload glyph matching the app's own refresh control (AppRefreshControl). */
|
|
300
|
+
function refreshIconSvg() {
|
|
301
|
+
return `
|
|
302
|
+
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
|
303
|
+
<path d="M20 6v5h-5"></path>
|
|
304
|
+
<path d="M4 18v-5h5"></path>
|
|
305
|
+
<path d="M18.2 9A7 7 0 0 0 6.1 6.8L4 9"></path>
|
|
306
|
+
<path d="M5.8 15a7 7 0 0 0 12.1 2.2L20 15"></path>
|
|
307
|
+
</svg>
|
|
308
|
+
`;
|
|
309
|
+
}
|
|
310
|
+
function contextKey(context) {
|
|
311
|
+
return `${context.machine.id}:${context.workspace.projectId}:${context.workspace.id}`;
|
|
312
|
+
}
|
|
313
|
+
function documentName(path) {
|
|
314
|
+
return path.slice(path.lastIndexOf("/") + 1);
|
|
315
|
+
}
|
|
316
|
+
function renderEmptyState() {
|
|
317
|
+
return `
|
|
318
|
+
<div class="empty-state">
|
|
319
|
+
<strong>No relays in this workspace.</strong>
|
|
320
|
+
<p>Relay packets live in <code>${escapeHtml(RELAYS_ROOT)}/<name>/</code>. This workspace has none yet.</p>
|
|
321
|
+
</div>
|
|
322
|
+
`;
|
|
323
|
+
}
|
|
324
|
+
function renderErrorState(message, detail) {
|
|
325
|
+
return `<div class="status error"><strong>${escapeHtml(message)}</strong><pre>${escapeHtml(detail)}</pre></div>`;
|
|
326
|
+
}
|
|
327
|
+
function relaysStyles() {
|
|
328
|
+
return `
|
|
329
|
+
<style>
|
|
330
|
+
:host { display: contents; }
|
|
331
|
+
/* Toolbar and tab strip are panel chrome: they must never flex-shrink
|
|
332
|
+
(the app container is a fixed-height flex column; with shrink enabled
|
|
333
|
+
the viewer's huge content basis starves them down to a sliver once a
|
|
334
|
+
tall document renders). The viewer absorbs all shrinking instead. */
|
|
335
|
+
.toolbar { flex: 0 0 auto; display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 10px 12px; border-bottom: 1px solid var(--pi-border-muted); }
|
|
336
|
+
.toolbar[hidden], .document-tabs[hidden] { display: none; }
|
|
337
|
+
.toolbar-actions { display: inline-flex; align-items: center; flex-wrap: nowrap; justify-content: flex-end; gap: 8px; min-width: 0; }
|
|
338
|
+
.relay-name { min-width: 0; color: var(--pi-text-secondary); overflow-wrap: anywhere; }
|
|
339
|
+
/* Bottom padding (not viewer margin) so the gap below the tabs persists
|
|
340
|
+
when the viewer's content scrolls up against its top edge. */
|
|
341
|
+
.document-tabs { flex: 0 0 auto; display: flex; flex-wrap: nowrap; gap: 6px; padding: 8px 12px; overflow-x: auto; overflow-y: hidden; overscroll-behavior-x: contain; scrollbar-width: thin; }
|
|
342
|
+
.viewer { flex: 1 1 auto; box-sizing: border-box; display: grid; align-content: start; gap: 12px; min-height: 0; overflow: auto; padding: 12px; }
|
|
343
|
+
/* Grid children default to min-width: auto; without these caps a wide code
|
|
344
|
+
block or table would silently stretch the whole viewer track. */
|
|
345
|
+
.viewer > * { box-sizing: border-box; min-width: 0; max-width: 100%; }
|
|
346
|
+
button, select { border: 1px solid var(--pi-border); border-radius: 7px; background: var(--pi-surface); color: var(--pi-text); font: inherit; }
|
|
347
|
+
button { cursor: pointer; padding: 6px 10px; }
|
|
348
|
+
button.icon-button { flex: 0 0 auto; display: inline-grid; place-items: center; width: 30px; height: 30px; padding: 0; }
|
|
349
|
+
button.icon-button svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; pointer-events: none; }
|
|
350
|
+
select { min-width: 0; max-width: 240px; padding: 5px 6px; }
|
|
351
|
+
.document-tab { flex: 0 0 auto; white-space: nowrap; font-size: 12px; padding: 4px 10px; }
|
|
352
|
+
.document-tab.active { border-color: var(--pi-accent-border); background: var(--pi-accent); color: var(--pi-bg); }
|
|
353
|
+
code, pre { border: 1px solid var(--pi-border-muted); border-radius: 6px; background: var(--pi-bg); color: var(--pi-text-secondary); font: 12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
|
354
|
+
code { padding: 2px 5px; }
|
|
355
|
+
pre { margin: 0; overflow: auto; padding: 8px; white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
356
|
+
.document.markdown { line-height: 1.5; overflow-wrap: anywhere; }
|
|
357
|
+
.document.markdown p, .document.markdown ul, .document.markdown ol, .document.markdown pre, .document.markdown blockquote, .document.markdown .table-scroll { margin: 0 0 10px; }
|
|
358
|
+
.document.markdown > :last-child { margin-bottom: 0; }
|
|
359
|
+
.document.markdown h1, .document.markdown h2, .document.markdown h3, .document.markdown h4 { line-height: 1.25; margin: 14px 0 8px; }
|
|
360
|
+
.document.markdown h1:first-child, .document.markdown h2:first-child, .document.markdown h3:first-child, .document.markdown h4:first-child { margin-top: 0; }
|
|
361
|
+
.document.markdown h1 { font-size: 18px; }
|
|
362
|
+
.document.markdown h2 { font-size: 16px; }
|
|
363
|
+
.document.markdown h3 { font-size: 14px; }
|
|
364
|
+
.document.markdown h4 { font-size: 13px; }
|
|
365
|
+
.document.markdown ul, .document.markdown ol { padding-left: 22px; }
|
|
366
|
+
.document.markdown li + li { margin-top: 3px; }
|
|
367
|
+
.document.markdown pre { white-space: pre; overflow-wrap: normal; }
|
|
368
|
+
.document.markdown pre code { border: 0; background: transparent; padding: 0; }
|
|
369
|
+
.document.markdown img { box-sizing: border-box; max-width: 100%; }
|
|
370
|
+
.document.markdown blockquote { border-left: 3px solid var(--pi-border-muted); color: var(--pi-muted); padding-left: 10px; }
|
|
371
|
+
.document.markdown a { color: var(--pi-accent); }
|
|
372
|
+
.document.markdown .table-scroll { max-width: 100%; overflow-x: auto; }
|
|
373
|
+
/* Cells wrap at word boundaries only: a wide table keeps its natural width
|
|
374
|
+
and scrolls inside .table-scroll instead of being squeezed unreadably. */
|
|
375
|
+
.document.markdown table { border-collapse: collapse; overflow-wrap: normal; }
|
|
376
|
+
.document.markdown th, .document.markdown td { border: 1px solid var(--pi-border-muted); padding: 4px 8px; }
|
|
377
|
+
.status pre { margin-top: 8px; }
|
|
378
|
+
.muted { color: var(--pi-muted); }
|
|
379
|
+
.empty-state { border: 1px dashed var(--pi-border-muted); border-radius: 8px; color: var(--pi-muted); padding: 12px; }
|
|
380
|
+
.empty-state p { margin: 6px 0 0; }
|
|
381
|
+
.status { border: 1px solid var(--pi-border); border-radius: 8px; padding: 10px; }
|
|
382
|
+
.status.info { border-color: var(--pi-accent-border); background: var(--pi-bg-overlay-soft); }
|
|
383
|
+
.status.error { border-color: var(--pi-danger); color: var(--pi-danger); }
|
|
384
|
+
.empty { padding: 16px; color: var(--pi-muted); }
|
|
385
|
+
</style>
|
|
386
|
+
`;
|
|
387
|
+
}
|
|
388
|
+
function escapeHtml(value) {
|
|
389
|
+
return String(value).replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
390
|
+
}
|
|
391
|
+
function escapeAttr(value) {
|
|
392
|
+
return escapeHtml(value).replaceAll('"', """);
|
|
393
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Vendored dependencies
|
|
2
|
+
|
|
3
|
+
## marked (`marked.esm.js`)
|
|
4
|
+
|
|
5
|
+
- **Source:** `node_modules/marked/lib/marked.esm.js` — marked v18.0.6, the
|
|
6
|
+
version the repo pins (`"marked": "^18.0.6"` in the root `package.json`).
|
|
7
|
+
- **License:** MIT — copyright (c) 2018-2026 MarkedJS, (c) 2011-2018
|
|
8
|
+
Christopher Jeffrey. The attribution header at the top of `marked.esm.js` is
|
|
9
|
+
preserved; see also `node_modules/marked/LICENSE.md`.
|
|
10
|
+
- **Why vendored:** bundled plugins load in the browser as standalone ES
|
|
11
|
+
modules and cannot resolve bare package specifiers. The plugin therefore
|
|
12
|
+
ships the exact marked build the repo already depends on, rather than
|
|
13
|
+
hand-rolling a markdown subset or adding a markdown helper to the plugin API.
|
|
14
|
+
- **Local modification:** the trailing `//# sourceMappingURL=marked.esm.js.map`
|
|
15
|
+
comment was removed because the (much larger) source map is not vendored.
|
|
16
|
+
Everything else is byte-identical to the published file.
|
|
17
|
+
- **`marked.esm.d.ts`** is a hand-written minimal declaration covering only the
|
|
18
|
+
surface `markdownDocument.ts` uses. The plugin build
|
|
19
|
+
(`scripts/build-plugins.mjs`) skips `.d.ts` files and copies `.js` assets
|
|
20
|
+
verbatim, so the vendored module ships as-is.
|
|
21
|
+
|
|
22
|
+
**Updating:** after a `marked` upgrade in the root `package.json`, copy the new
|
|
23
|
+
`node_modules/marked/lib/marked.esm.js` here, re-apply the sourceMappingURL
|
|
24
|
+
removal, and update this note.
|