@starlove/orbit 1.2.0 → 1.3.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/public/index.html CHANGED
@@ -37,10 +37,10 @@
37
37
  <!-- Boot splash — the canonical ss-ui loading screen (see the package's
38
38
  components/loader.css). Inline critical CSS so it paints before
39
39
  /js/main.js loads. The logo grows in then breathes; no spinner.
40
- Keyframes namespaced `ss-loader-*`. Under reduced motion the grow-in
40
+ Keyframes namespaced `starscape-loader-*`. Under reduced motion the grow-in
41
41
  is dropped and the dot grid stops; the gentle breathe stays. -->
42
42
  <style>
43
- .ss-loader {
43
+ .loader {
44
44
  position: fixed;
45
45
  inset: 0;
46
46
  z-index: 99999;
@@ -60,7 +60,7 @@
60
60
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI",
61
61
  Roboto, "Helvetica Neue", Arial, sans-serif;
62
62
  }
63
- .ss-loader::before {
63
+ .loader::before {
64
64
  content: "";
65
65
  position: absolute;
66
66
  width: 200%;
@@ -72,24 +72,28 @@
72
72
  );
73
73
  background-size: 50px 50px;
74
74
  will-change: transform;
75
- animation: ss-loader-drift 20s linear infinite;
75
+ animation: starscape-loader-drift 20s linear infinite;
76
76
  }
77
- .ss-loader.is-hiding {
77
+ .loader.is-hiding {
78
78
  opacity: 0;
79
79
  pointer-events: none;
80
80
  }
81
- .ss-loader__logo {
81
+ .loader__logo {
82
82
  width: 120px;
83
83
  height: 120px;
84
84
  object-fit: contain;
85
85
  filter: drop-shadow(0 0 20px rgba(255, 255, 255, 0.3));
86
86
  transform-origin: center;
87
+ /* translateZ in the keyframes + backface-visibility keep the star
88
+ on a stable GPU layer, so it doesn't snap ~1px as the grow
89
+ starts. Keep in sync with the package components/loader.css. */
90
+ backface-visibility: hidden;
87
91
  will-change: transform;
88
92
  animation:
89
- ss-loader-grow 0.6s cubic-bezier(0.22, 1, 0.36, 1) both,
90
- ss-loader-breathe 3s ease-in-out 0.6s infinite;
93
+ starscape-loader-grow 0.6s cubic-bezier(0.22, 1, 0.36, 1) both,
94
+ starscape-loader-breathe 3s ease-in-out 0.6s infinite;
91
95
  }
92
- .ss-loader__label {
96
+ .loader__label {
93
97
  margin: 0;
94
98
  color: #fff;
95
99
  font-size: 1.25rem;
@@ -97,45 +101,45 @@
97
101
  letter-spacing: 0.5px;
98
102
  text-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
99
103
  }
100
- @keyframes ss-loader-drift {
104
+ @keyframes starscape-loader-drift {
101
105
  from { transform: translate(0, 0); }
102
106
  to { transform: translate(-50px, -50px); }
103
107
  }
104
- @keyframes ss-loader-grow {
105
- from { transform: scale(0); opacity: 0; }
106
- to { transform: scale(1); opacity: 1; }
108
+ @keyframes starscape-loader-grow {
109
+ from { transform: scale(0) translateZ(0); opacity: 0; }
110
+ to { transform: scale(1) translateZ(0); opacity: 1; }
107
111
  }
108
- @keyframes ss-loader-breathe {
109
- 0%, 100% { transform: scale(1); }
110
- 50% { transform: scale(1.06); }
112
+ @keyframes starscape-loader-breathe {
113
+ 0%, 100% { transform: scale(1) translateZ(0); }
114
+ 50% { transform: scale(1.06) translateZ(0); }
111
115
  }
112
116
  @media (prefers-reduced-motion: reduce) {
113
- :root:not([data-motion="full"]) .ss-loader::before {
117
+ :root:not([data-motion="full"]) .loader::before {
114
118
  animation: none;
115
119
  }
116
- :root:not([data-motion="full"]) .ss-loader__logo {
117
- animation: ss-loader-breathe 3s ease-in-out infinite;
120
+ :root:not([data-motion="full"]) .loader__logo {
121
+ animation: starscape-loader-breathe 3s ease-in-out infinite;
118
122
  }
119
123
  }
120
- [data-motion="reduce"] .ss-loader::before {
124
+ [data-motion="reduce"] .loader::before {
121
125
  animation: none;
122
126
  }
123
- [data-motion="reduce"] .ss-loader__logo {
124
- animation: ss-loader-breathe 3s ease-in-out infinite;
127
+ [data-motion="reduce"] .loader__logo {
128
+ animation: starscape-loader-breathe 3s ease-in-out infinite;
125
129
  }
126
130
  </style>
127
131
  </head>
128
132
  <body>
129
133
  <!-- Boot splash - shown immediately, removed once the app is ready. -->
130
- <div id="ss-loader" class="ss-loader" role="status" aria-live="polite">
134
+ <div id="loader" class="loader" role="status" aria-live="polite">
131
135
  <img
132
- class="ss-loader__logo"
136
+ class="loader__logo"
133
137
  src="./orbits-logo.png"
134
138
  alt="Orbit"
135
139
  width="120"
136
140
  height="120"
137
141
  />
138
- <p class="ss-loader__label">Loading Orbit…</p>
142
+ <p class="loader__label">Loading Orbit…</p>
139
143
  </div>
140
144
 
141
145
  <div class="grain">
@@ -143,9 +147,40 @@
143
147
  </div>
144
148
  <header class="topbar">
145
149
  <div class="brand" aria-label="Orbit">
146
- <img class="mark" src="./orbits-logo.png" alt="Orbits logo" />
150
+ <button
151
+ id="brandFocusBtn"
152
+ type="button"
153
+ class="brand-focus-btn"
154
+ data-variant="brand-focus"
155
+ aria-label="Show board notes"
156
+ aria-expanded="false"
157
+ aria-controls="brandFocusPopover"
158
+ >
159
+ <img class="mark" src="./orbits-logo.png" alt="Orbits logo" />
160
+ </button>
161
+ <div
162
+ id="brandFocusPopover"
163
+ class="brand-focus-popover"
164
+ role="dialog"
165
+ aria-label="Board notes"
166
+ hidden
167
+ >
168
+ <div class="brand-focus-popover__eyebrow">Board notes</div>
169
+ <div id="brandFocusNotes" class="brand-focus-notes-body">No board notes yet</div>
170
+ </div>
147
171
  </div>
148
172
  <div class="controls">
173
+ <button
174
+ id="brandNotesBtn"
175
+ type="button"
176
+ class="topbar-notes brand-notes-strip"
177
+ data-variant="topbar-notes"
178
+ aria-label="Show board notes"
179
+ aria-expanded="false"
180
+ aria-controls="brandFocusPopover"
181
+ >
182
+ <span id="brandNotesText" class="topbar-notes__text"></span>
183
+ </button>
149
184
  <span class="topbar-controls-gap" aria-hidden="true"></span>
150
185
  <div class="topbar-search" id="topbarSearch">
151
186
  <input
@@ -235,6 +270,32 @@
235
270
  />
236
271
  </svg>
237
272
  </button>
273
+ <button
274
+ id="helpBtn"
275
+ type="button"
276
+ class="topbar-btn topbar-ctl-shrink"
277
+ data-variant="translucent"
278
+ aria-haspopup="dialog"
279
+ aria-controls="helpDialog"
280
+ >
281
+ <svg
282
+ class="topbar-icon"
283
+ width="16"
284
+ height="16"
285
+ viewBox="0 0 24 24"
286
+ fill="none"
287
+ stroke="currentColor"
288
+ stroke-width="2"
289
+ stroke-linecap="round"
290
+ stroke-linejoin="round"
291
+ aria-hidden="true"
292
+ >
293
+ <circle cx="12" cy="12" r="10" />
294
+ <path d="M9.09 9a3 3 0 015.83 1c0 2-3 3-3 3" />
295
+ <path d="M12 17h.01" />
296
+ </svg>
297
+ Help
298
+ </button>
238
299
  <button
239
300
  id="settingsBtn"
240
301
  type="button"
@@ -424,6 +485,141 @@
424
485
  </div>
425
486
  </dialog>
426
487
 
488
+ <!-- Help & About. Reuses the .orbit-dialog shell + welcome-dialog-*
489
+ styling so it inherits the modal chrome; opened from the topbar
490
+ Help button and wired through modal.js (Escape/backdrop close). -->
491
+ <dialog
492
+ id="helpDialog"
493
+ class="orbit-dialog"
494
+ aria-labelledby="helpDialogTitle"
495
+ >
496
+ <div class="welcome-dialog-inner help-dialog-inner">
497
+ <div class="welcome-dialog-head">
498
+ <img
499
+ class="welcome-dialog-logo"
500
+ src="./orbits-logo.png"
501
+ alt=""
502
+ aria-hidden="true"
503
+ />
504
+ <span
505
+ class="welcome-dialog-edition-pill"
506
+ id="helpDialogVersion"
507
+ >Orbit</span
508
+ >
509
+ <button
510
+ type="button"
511
+ class="help-dialog-close"
512
+ aria-label="Close"
513
+ data-modal-close
514
+ >
515
+ ×
516
+ </button>
517
+ </div>
518
+ <div class="welcome-dialog-body">
519
+ <h2 id="helpDialogTitle" class="welcome-dialog-title">
520
+ Starscape Orbit
521
+ </h2>
522
+ <p class="welcome-dialog-lede">
523
+ A developer-first kanban board built for teams working
524
+ alongside AI agents. Track the motion of your project —
525
+ and hand work to your coding agent through the Orbit MCP
526
+ tools.
527
+ </p>
528
+
529
+ <section
530
+ class="help-dialog-section"
531
+ aria-label="Getting started"
532
+ >
533
+ <h3 class="help-dialog-heading">Getting started</h3>
534
+ <ol class="help-dialog-steps">
535
+ <li class="help-dialog-step">
536
+ <span class="help-dialog-step__index">01</span>
537
+ <div>
538
+ <h4>Create a board</h4>
539
+ <p>
540
+ Use the <strong>Board</strong> menu →
541
+ New board. Optionally point it at a
542
+ coding project folder so Orbit can drop
543
+ <code>SKILL-ORBIT.md</code> and
544
+ <code>AGENTS.md</code> for your agent.
545
+ </p>
546
+ </div>
547
+ </li>
548
+ <li class="help-dialog-step">
549
+ <span class="help-dialog-step__index">02</span>
550
+ <div>
551
+ <h4>Add cards &amp; the AI Ready lane</h4>
552
+ <p>
553
+ Capture work with
554
+ <strong>New Card</strong>. Move cards
555
+ into <strong>AI Ready</strong> to stage
556
+ them for an agent to claim.
557
+ </p>
558
+ </div>
559
+ </li>
560
+ <li class="help-dialog-step">
561
+ <span class="help-dialog-step__index">03</span>
562
+ <div>
563
+ <h4>Connect your coding agent</h4>
564
+ <p>
565
+ Add the Orbit MCP server from
566
+ <strong>Settings</strong>, then ask your
567
+ agent to read <code>AGENTS.md</code> and
568
+ claim the next ticket.
569
+ </p>
570
+ </div>
571
+ </li>
572
+ </ol>
573
+ </section>
574
+
575
+ <section
576
+ class="help-dialog-section"
577
+ aria-label="Links"
578
+ >
579
+ <h3 class="help-dialog-heading">Links</h3>
580
+ <ul class="help-dialog-links">
581
+ <li>
582
+ <a
583
+ class="welcome-dialog-link"
584
+ href="https://orbit.starscape.app"
585
+ target="_blank"
586
+ rel="noopener"
587
+ >Documentation</a
588
+ >
589
+ </li>
590
+ <li>
591
+ <a
592
+ class="welcome-dialog-link"
593
+ href="https://github.com/brucestarlove/ss-orbit/blob/master/docs/MCP_SETUP.md"
594
+ target="_blank"
595
+ rel="noopener"
596
+ >MCP setup</a
597
+ >
598
+ </li>
599
+ <li>
600
+ <a
601
+ class="welcome-dialog-link"
602
+ href="https://orbit.starscape.app/support"
603
+ target="_blank"
604
+ rel="noopener"
605
+ >Support / Tip</a
606
+ >
607
+ </li>
608
+ </ul>
609
+ </section>
610
+ </div>
611
+ <footer class="welcome-dialog-footer">
612
+ <button
613
+ type="button"
614
+ class="welcome-dialog-cta"
615
+ data-modal-close
616
+ >
617
+ Got it
618
+ </button>
619
+ </footer>
620
+ </div>
621
+ </dialog>
622
+
427
623
  <script src="/js/main.js" type="module"></script>
428
624
  <!-- Fade out the boot splash once the DOM is ready. The /js/main.js
429
625
  module runs before DOMContentLoaded, so theme/motion + wiring are
@@ -431,7 +627,7 @@
431
627
  <script>
432
628
  (function () {
433
629
  function hideSplash() {
434
- var splash = document.getElementById("ss-loader");
630
+ var splash = document.getElementById("loader");
435
631
  if (!splash) return;
436
632
  // Keep the splash up for a minimum of 786ms (measured from
437
633
  // navigation start) so a fast load doesn't flash it in and out.
package/public/js/app.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // modules can import `load` / `render` without pulling in the entry-point
3
3
  // init wiring (which would create import cycles).
4
4
 
5
- import { state, syncBoardSelection } from "./state.js";
5
+ import { state, syncBoardSelection, currentBoard } from "./state.js";
6
6
  import { api, bootstrapPath } from "./api.js";
7
7
  import { renderBoard, renderBoardSelection } from "./kanban.js";
8
8
  import { renderDetail } from "./ticket-detail.js";
@@ -10,6 +10,7 @@ import { closeMenuFlyouts, updateTopbarChips } from "./board-menu.js";
10
10
  import { connectEventStream } from "./sse.js";
11
11
  import { seedReadIfNeeded } from "./unread.js";
12
12
  import { features } from "./config.js";
13
+ import { renderTopbarFocus } from "./topbar-notes.js";
13
14
 
14
15
  let sseBoardId = null;
15
16
 
@@ -30,6 +31,7 @@ export async function load() {
30
31
  export async function render() {
31
32
  syncBoardSelection();
32
33
  updateTopbarChips();
34
+ renderTopbarFocus(currentBoard());
33
35
  renderBoard();
34
36
  await renderDetail();
35
37
  }
@@ -37,6 +39,7 @@ export async function render() {
37
39
  export async function renderDetailOnly() {
38
40
  syncBoardSelection();
39
41
  updateTopbarChips();
42
+ renderTopbarFocus(currentBoard());
40
43
  renderBoardSelection();
41
44
  await renderDetail();
42
45
  }
@@ -65,12 +65,18 @@ function renderBoardFlyoutInner() {
65
65
  <div class="menu-flyout-section">
66
66
  <h3 class="menu-flyout-heading">New board</h3>
67
67
  <form id="createBoardForm" class="menu-flyout-form">
68
+ <input name="name" type="text" required placeholder="Board name" autocomplete="off" />
68
69
  <div class="folder-picker-field">
69
- <input id="repoPathInput" name="repo_path" type="text" required readonly placeholder="Choose a folder" autocomplete="off" />
70
+ <input id="repoPathInput" name="repo_path" type="text" readonly placeholder="Coding project folder (optional)" autocomplete="off" />
70
71
  <button type="button" id="pickRepoFolderBtn">Browse</button>
71
72
  </div>
72
- <p class="menu-flyout-hint menu-flyout-hint--intro">Board data is stored in <code>.orbit/</code> under this folder.</p>
73
- <input name="name" type="text" required placeholder="Board name" autocomplete="off" />
73
+ <div id="helperFilesReveal" hidden>
74
+ <p class="menu-flyout-hint menu-flyout-hint--intro">Orbit writes <code>SKILL-ORBIT.md</code> and <code>AGENTS.md</code> into this folder for your AI coding agent.</p>
75
+ <label class="menu-flyout-check">
76
+ <input id="helperFilesCheckbox" name="manage_helper_files" type="checkbox" checked />
77
+ <span>Generate AI helper files (<code>SKILL-ORBIT.md</code>, <code>AGENTS.md</code>)</span>
78
+ </label>
79
+ </div>
74
80
  <input name="slug" type="text" placeholder="Slug (optional)" autocomplete="off" />
75
81
  <input name="default_branch" type="text" placeholder="Default branch (optional)" autocomplete="off" />
76
82
  <input name="repo_url" type="text" placeholder="Repo URL (optional)" autocomplete="off" />
@@ -103,6 +109,14 @@ function wireBoardFlyout() {
103
109
  if (form) {
104
110
  const pickRepoFolderBtn = form.querySelector("#pickRepoFolderBtn");
105
111
  const repoPathInput = form.querySelector("#repoPathInput");
112
+ const helperFilesReveal = form.querySelector("#helperFilesReveal");
113
+ // The folder explanation and "Generate AI helper files" option only make
114
+ // sense once a folder is chosen — there's nowhere to write them otherwise.
115
+ // Reveal both with the folder value rather than disabling.
116
+ const syncHelperFilesVisibility = () => {
117
+ if (helperFilesReveal) helperFilesReveal.hidden = !repoPathInput.value.trim();
118
+ };
119
+ syncHelperFilesVisibility();
106
120
  pickRepoFolderBtn?.addEventListener("click", async (event) => {
107
121
  event.preventDefault();
108
122
  pickRepoFolderBtn.disabled = true;
@@ -111,6 +125,7 @@ function wireBoardFlyout() {
111
125
  if (result.path) {
112
126
  repoPathInput.value = result.path;
113
127
  repoPathInput.dispatchEvent(new Event("input", { bubbles: true }));
128
+ syncHelperFilesVisibility();
114
129
  }
115
130
  } catch (error) {
116
131
  toast.error(error?.message || "Folder picker unavailable");
@@ -125,15 +140,13 @@ function wireBoardFlyout() {
125
140
  const branch = String(fd.get("default_branch") || "").trim();
126
141
  const repo = String(fd.get("repo_url") || "").trim();
127
142
  const repoPath = String(fd.get("repo_path") || "").trim();
128
- if (!repoPath) {
129
- repoPathInput?.reportValidity();
130
- return;
131
- }
143
+ // Folder is optional. Only forward the helper-files choice when a folder
144
+ // is set; the server ignores it otherwise.
132
145
  const created = await api("/api/boards", {
133
146
  method: "POST",
134
147
  body: {
135
148
  name: fd.get("name"),
136
- repo_path: repoPath,
149
+ ...(repoPath ? { repo_path: repoPath, manage_helper_files: fd.get("manage_helper_files") === "on" } : {}),
137
150
  ...(slug ? { slug } : {}),
138
151
  ...(branch ? { default_branch: branch } : {}),
139
152
  ...(repo ? { repo_url: repo } : {})
@@ -0,0 +1,31 @@
1
+ // Help & About dialog. The topbar "Help" button opens a centered
2
+ // .orbit-dialog (same shell as the welcome dialog) describing what Orbit is,
3
+ // how to get started, and where the docs live. modal.js owns the
4
+ // Escape/backdrop/close-button behavior; this module only wires the open
5
+ // trigger and best-effort fills the version pill from the build's version.json.
6
+
7
+ import { openModal, wireModal } from "./modal.js";
8
+
9
+ async function fillVersion(pill) {
10
+ if (!pill) return;
11
+ try {
12
+ const res = await fetch("./version.json", { cache: "no-store" });
13
+ if (!res.ok) return;
14
+ const info = await res.json();
15
+ if (info?.version) pill.textContent = `v${info.version}`;
16
+ } catch {
17
+ // Dev edition serves raw modules without version.json — leave the
18
+ // static "Orbit" label in place.
19
+ }
20
+ }
21
+
22
+ export function wireHelpMenu() {
23
+ const dialog = document.getElementById("helpDialog");
24
+ const helpBtn = document.getElementById("helpBtn");
25
+ if (!dialog || !helpBtn) return;
26
+
27
+ wireModal(dialog);
28
+ fillVersion(document.getElementById("helpDialogVersion"));
29
+
30
+ helpBtn.addEventListener("click", () => openModal(dialog));
31
+ }
package/public/js/main.js CHANGED
@@ -41,10 +41,14 @@ import { runSearch, hideSearch } from "./search.js";
41
41
  import { load } from "./app.js";
42
42
  import { edition } from "./config.js";
43
43
  import { openModal, wireModal } from "./modal.js";
44
+ import { wireTopbarFocus } from "./topbar-notes.js";
45
+ import { wireHelpMenu } from "./help-menu.js";
44
46
 
45
47
  async function init() {
46
48
  const welcomeDialog = document.getElementById("welcomeDialog");
47
49
  wireModal(welcomeDialog);
50
+ wireTopbarFocus();
51
+ wireHelpMenu();
48
52
  if (edition === "preview") {
49
53
  openModal(welcomeDialog);
50
54
  }
@@ -118,6 +118,11 @@ const MCP_CLIENTS = {
118
118
  format: "MCP JSON",
119
119
  helper: "Paste this JSON into Cursor MCP settings. Cursor starts Orbit when an agent needs the tools."
120
120
  },
121
+ vscode: {
122
+ label: "VS Code",
123
+ format: "mcp.json",
124
+ helper: "Paste this JSON into VS Code's user or workspace MCP configuration."
125
+ },
121
126
  opencode: {
122
127
  label: "OpenCode",
123
128
  format: "opencode.json",
@@ -138,8 +143,9 @@ const MCP_CLIENTS = {
138
143
  function renderProjectAiTab(context) {
139
144
  const project = context.board;
140
145
  const aiEnabled = project.ai_enabled !== 0;
141
- const aiDivider = aiEnabled ? `<div class="ai-section-divider" aria-hidden="true"></div>` : "";
146
+ const aiDivider = aiEnabled ? `<div class="separator" aria-hidden="true"></div>` : "";
142
147
  const agentContextSetup = aiEnabled ? renderAgentContextSection(project) : "";
148
+ const helperFilesSetup = aiEnabled && features.multiBoard ? renderHelperFilesSection(project) : "";
143
149
  const mcpSetup = aiEnabled ? renderMcpSetupSection(context) : "";
144
150
 
145
151
  return `
@@ -159,6 +165,7 @@ function renderProjectAiTab(context) {
159
165
  </div>
160
166
  ${aiDivider}
161
167
  ${agentContextSetup}
168
+ ${helperFilesSetup ? `${aiDivider}${helperFilesSetup}` : ""}
162
169
  ${aiDivider}
163
170
  ${mcpSetup}
164
171
  `;
@@ -174,7 +181,7 @@ function renderAgentContextSection(project) {
174
181
  return `
175
182
  <div class="section ai-subsection ai-context-setup">
176
183
  <div class="ai-context-title-row">
177
- <span class="ai-context-inline-mark" aria-hidden="true">Context</span>
184
+ <span class="section-mark" aria-hidden="true">Context</span>
178
185
  <h3>Agent Instructions</h3>
179
186
  </div>
180
187
  <p class="description">This project-level context is provided to agents when they work on tickets.</p>
@@ -203,7 +210,7 @@ function renderMcpSetupSection(context) {
203
210
  <details class="section ai-subsection mcp-setup" data-mcp-setup>
204
211
  <summary>
205
212
  <div class="mcp-title-row">
206
- <span class="mcp-inline-mark" aria-hidden="true">MCP</span>
213
+ <span class="section-mark" aria-hidden="true">MCP</span>
207
214
  <h3>Connect Your Agent to MCP</h3>
208
215
  </div>
209
216
  </summary>
@@ -302,6 +309,7 @@ function mcpConfigText(clientId, paths) {
302
309
  if (clientId === "claude-code") return claudeCodeMcpCommand(paths);
303
310
  if (clientId === "codex") return codexMcpToml(paths);
304
311
  if (clientId === "opencode") return opencodeMcpJson(paths);
312
+ if (clientId === "vscode") return vscodeMcpJson(paths);
305
313
 
306
314
  const config = {
307
315
  mcpServers: {
@@ -331,6 +339,22 @@ function opencodeMcpJson(paths) {
331
339
  );
332
340
  }
333
341
 
342
+ function vscodeMcpJson(paths) {
343
+ return JSON.stringify(
344
+ {
345
+ servers: {
346
+ starscapeOrbit: {
347
+ type: "stdio",
348
+ command: "node",
349
+ args: [paths.serverPath]
350
+ }
351
+ }
352
+ },
353
+ null,
354
+ 2
355
+ );
356
+ }
357
+
334
358
  function codexMcpToml(paths) {
335
359
  return `[mcp_servers.starscape-orbit]
336
360
  command = "node"
@@ -426,6 +450,31 @@ function renderProjectRepositoryTab(context) {
426
450
  `;
427
451
  }
428
452
 
453
+ function renderHelperFilesSection(project) {
454
+ const hasFolder = Boolean(project.system_path);
455
+ const managed = Boolean(project.manage_helper_files);
456
+ return `
457
+ <div class="section ai-subsection helper-files-section">
458
+ <div class="ai-context-title-row">
459
+ <span class="section-mark" aria-hidden="true">Files</span>
460
+ <h3>AI Helper Files</h3>
461
+ </div>
462
+ <p class="description">Orbit can keep <code>SKILL-ORBIT.md</code> and <code>AGENTS.md</code> in this board's coding project folder so your AI agent knows how to use Orbit.${hasFolder ? "" : " Link a folder to enable this."}</p>
463
+ <div class="folder-picker-field helper-files-folder">
464
+ <input id="helperFolderInput" type="text" readonly placeholder="No folder linked" value="${escapeHtml(project.system_path || "")}" />
465
+ <button type="button" id="helperPickFolderBtn">${hasFolder ? "Change" : "Browse"}</button>
466
+ </div>
467
+ <label class="orbit-check">
468
+ <input type="checkbox" id="manageHelperFilesToggle" ${managed ? "checked" : ""} ${hasFolder ? "" : "disabled"} />
469
+ <span class="orbit-check-box" aria-hidden="true">
470
+ <svg viewBox="0 0 16 16" class="orbit-check-tick"><path d="M3.5 8.5l3 3 6-7" /></svg>
471
+ </span>
472
+ <span>Generate &amp; maintain helper files in this folder</span>
473
+ </label>
474
+ </div>
475
+ `;
476
+ }
477
+
429
478
  function renderBoardRenameSection(project) {
430
479
  return `
431
480
  <div class="section board-rename-section">
@@ -953,6 +1002,49 @@ function bindProjectTabHandlers(context, tab) {
953
1002
  toast.success(enabled ? "AI features enabled — agent lanes provisioned" : "AI features disabled — AI Ready hidden");
954
1003
  });
955
1004
 
1005
+ const helperPickFolderBtn = $("#helperPickFolderBtn");
1006
+ helperPickFolderBtn?.addEventListener("click", async () => {
1007
+ helperPickFolderBtn.disabled = true;
1008
+ try {
1009
+ const result = await api("/api/system/pick-folder", { method: "POST" });
1010
+ if (!result.path) return;
1011
+ await api(`/api/boards/${encodeURIComponent(project.id)}`, {
1012
+ method: "PATCH",
1013
+ body: { system_path: result.path }
1014
+ });
1015
+ await load();
1016
+ state.detailMode = "settings";
1017
+ toast.success("Linked coding project folder updated");
1018
+ } catch (error) {
1019
+ toast.error(error?.message || "Folder picker unavailable");
1020
+ } finally {
1021
+ helperPickFolderBtn.disabled = false;
1022
+ }
1023
+ });
1024
+
1025
+ const manageHelperFilesToggle = $("#manageHelperFilesToggle");
1026
+ manageHelperFilesToggle?.addEventListener("change", async () => {
1027
+ const enabled = manageHelperFilesToggle.checked;
1028
+ manageHelperFilesToggle.disabled = true;
1029
+ try {
1030
+ await api(`/api/boards/${encodeURIComponent(project.id)}`, {
1031
+ method: "PATCH",
1032
+ body: { manage_helper_files: enabled }
1033
+ });
1034
+ await load();
1035
+ state.detailMode = "settings";
1036
+ toast.success(
1037
+ enabled
1038
+ ? "Generated SKILL-ORBIT.md and AGENTS.md — Orbit will keep them in sync"
1039
+ : "Stopped maintaining helper files (existing files left in place)"
1040
+ );
1041
+ } catch (error) {
1042
+ manageHelperFilesToggle.checked = !enabled;
1043
+ manageHelperFilesToggle.disabled = false;
1044
+ toast.error(error?.message || "Could not update helper files setting");
1045
+ }
1046
+ });
1047
+
956
1048
  }
957
1049
 
958
1050
  if (tab === "archive") {