@monster-orange/den 0.4.10 → 0.4.11

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/CHANGELOG.md CHANGED
@@ -4,6 +4,31 @@ All notable changes to Den are documented here. Each release lists the
4
4
  config and manifest changes a distribution must make to move to it under
5
5
  **Migrations**.
6
6
 
7
+ ## [0.4.11] - 2026-11-07
8
+
9
+ ### Changed
10
+
11
+ - **Picking a workspace in the top bar also makes it the CLI default.**
12
+ There is one "current workspace": choosing one in the Workspace menu
13
+ sets what pages show and where `<cli>` commands run (workspaces.yml, as
14
+ Local Dev's "Make CLI default" does). "All active (n)" changes only what
15
+ pages show; the CLI default stays. If the CLI default can't be set,
16
+ nothing changes and the error shows.
17
+ - **The menu's separate "Make <ws> the CLI default" item is gone**, since
18
+ picking a workspace does it. The "CLI default" tag stays on its row, and
19
+ Local Dev keeps its "Make CLI default" button, to set it without
20
+ changing what pages show. Each workspace row's tooltip says so.
21
+ - **Picking the workspace already in view makes it the CLI default** when
22
+ it isn't one (after Local Dev's "Make CLI default" or `<cli> workspace
23
+ focus` moved it). The view stays as it is.
24
+ - **Pages whose address names a workspace follow the pick.** Workspaces'
25
+ detail pane and breadcrumb now show the workspace the top bar picked,
26
+ not the one in the old `?ws=`.
27
+
28
+ ### Migrations
29
+
30
+ None.
31
+
7
32
  ## [0.4.10] - 2026-11-07
8
33
 
9
34
  ### Changed
package/README.md CHANGED
@@ -10,8 +10,8 @@
10
10
  </p>
11
11
 
12
12
  <picture>
13
- <source media="(prefers-color-scheme: dark)" srcset="docs/images/0.4.10/hero-dark.png">
14
- <img src="docs/images/0.4.10/hero-light.png" width="100%" alt="Den's Graph page: the demo workspace's services grouped by application, all healthy, with the status chip and the left rail">
13
+ <source media="(prefers-color-scheme: dark)" srcset="docs/images/0.4.11/hero-dark.png">
14
+ <img src="docs/images/0.4.11/hero-light.png" width="100%" alt="Den's Graph page: the demo workspace's services grouped by application, all healthy, with the status chip and the left rail">
15
15
  </picture>
16
16
 
17
17
  ## Set up by your agent
@@ -38,7 +38,7 @@ It doesn't replace your tools. Your repos, Docker, your test runners, your secre
38
38
  yours. Den connects them and turns on good practice from the start.
39
39
 
40
40
  <p align="center">
41
- <img src="docs/images/0.4.10/up.gif" width="100%" alt="make up in the Graph page: every stack down, then demo-api, demo-web and demo-notes come up in dependency order until the status chip reads All 3 healthy">
41
+ <img src="docs/images/0.4.11/up.gif" width="100%" alt="make up in the Graph page: every stack down, then demo-api, demo-web and demo-notes come up in dependency order until the status chip reads All 3 healthy">
42
42
  </p>
43
43
 
44
44
  ## How it helps
@@ -68,10 +68,10 @@ yours. Den connects them and turns on good practice from the start.
68
68
  lives in your own repo.
69
69
 
70
70
  <p align="center">
71
- <img src="docs/images/0.4.10/agents.png" width="49%" alt="The Agents tab: the demo workspace's sessions on the left, and one open in a terminal pane on the right, running demo-api's test and then the service">
72
- <img src="docs/images/0.4.10/workspaces.png" width="49%" alt="The Workspaces page: the demo workspace's repos with their health, mode and Start, Stop, Restart and Logs actions">
73
- <img src="docs/images/0.4.10/status-drawer.png" width="49%" alt="The status drawer: nothing needs attention, and every service in the demo workspace, by application, with its latency">
74
- <img src="docs/images/0.4.10/edge.png" width="49%" alt="The Edge page: the edge container up, its ports and TLS, and every route it serves, Den's own and each stack's, with its upstream">
71
+ <img src="docs/images/0.4.11/agents.png" width="49%" alt="The Agents tab: the demo workspace's sessions on the left, and one open in a terminal pane on the right, running demo-api's test and then the service">
72
+ <img src="docs/images/0.4.11/workspaces.png" width="49%" alt="The Workspaces page: the demo workspace's repos with their health, mode and Start, Stop, Restart and Logs actions">
73
+ <img src="docs/images/0.4.11/status-drawer.png" width="49%" alt="The status drawer: nothing needs attention, and every service in the demo workspace, by application, with its latency">
74
+ <img src="docs/images/0.4.11/edge.png" width="49%" alt="The Edge page: the edge container up, its ports and TLS, and every route it serves, Den's own and each stack's, with its upstream">
75
75
  </p>
76
76
 
77
77
  ## Get started
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monster-orange/den",
3
- "version": "0.4.10",
3
+ "version": "0.4.11",
4
4
  "description": "Den: a local multi-repo dev orchestrator, HTTPS edge and E2E harness",
5
5
  "type": "module",
6
6
  "bin": {
@@ -792,10 +792,11 @@ export function groupReposByApplication(repos, apps) {
792
792
 
793
793
  /**
794
794
  * Feed the shell's Workspace part (shell.js #ws-pill, a menu): "All active"
795
- * and each active workspace, the focused one marked. Choosing one sets the
796
- * view scope (POST /api/scope, a preference) for every page. It replaces
797
- * dev.17's workspace pill and its focus menu (one control, not two): the
798
- * focus moves only through the menu's separate "Set <ws> as focus".
795
+ * and each active workspace, the focused one marked. Choosing a workspace
796
+ * makes it the CLI default (POST /api/workspaces `focus`, workspaces.yml)
797
+ * and sets the view (POST /api/scope, a preference) for every page: one
798
+ * "current workspace" (Ward's decision, 0.4.11). "All active" sets only the
799
+ * view; the CLI default stays.
799
800
  *
800
801
  * The arguments are dev.17's (an env select, an onSwitch callback) and are
801
802
  * no longer used: the view applies to every env.
@@ -822,7 +823,6 @@ async function _renderScopeMenu() {
822
823
  sh.setWorkspaces({
823
824
  ...buildScopeMenuModel(pageScope(scope, data), data, t),
824
825
  onSelect: _switchScope,
825
- onFocus: _setFocus,
826
826
  });
827
827
  } catch {
828
828
  sh.setWorkspaces(null);
@@ -852,25 +852,34 @@ function pageScope(scope, data) {
852
852
  }
853
853
 
854
854
  /**
855
- * On the Graph page a choice replaces ?ws= (All active drops it, a
856
- * workspace sets it), so the reload shows the choice rather than the old
857
- * ?ws=, which would win over it. The history entry is replaced, not
858
- * pushed: Back does not return to a view the picker has left.
855
+ * A choice replaces ?ws= (All active drops it, a workspace sets it) on the
856
+ * Graph and Agents pages, and on any page whose URL names a workspace
857
+ * (Workspaces' detail pane), so the reload shows the choice rather than
858
+ * the old ?ws=. The history entry is replaced, not pushed: Back does not
859
+ * return to a view the picker has left.
859
860
  */
860
861
  function followScopeInUrl(value) {
861
- if (!WS_URL_PAGES.has(location.pathname)) return;
862
862
  const u = new URL(location.href);
863
+ if (!WS_URL_PAGES.has(location.pathname) && !u.searchParams.has("ws")) return;
863
864
  if (value === "all") u.searchParams.delete("ws");
864
865
  else u.searchParams.set("ws", value);
865
866
  history.replaceState(history.state, "", u.pathname + u.search + u.hash);
866
867
  }
867
868
 
868
869
  /**
869
- * A view filter only: POST /api/scope stores the preference (user.yml
870
- * `view`), then the page reloads to show the new view whole. No service,
871
- * container, daemon or route changes, and the focus stays where it is.
870
+ * A workspace: first the CLI default (workspaces.yml), then the view. When
871
+ * the CLI default can't be set, nothing changes and the error shows. "All
872
+ * active": the view only. POST /api/scope stores the view (user.yml
873
+ * `view`), then the page reloads to show it whole. The workspace already
874
+ * in view (`inView`): the CLI default only, the view untouched. No
875
+ * service, container, daemon or route changes.
872
876
  */
873
- async function _switchScope(value) {
877
+ async function _switchScope(value, focus, inView) {
878
+ if (value !== "all" && value !== focus && !(await _setFocus(value))) return;
879
+ if (inView) {
880
+ location.reload();
881
+ return;
882
+ }
874
883
  try {
875
884
  await window.denShell.api("POST", "/api/scope", { scope: value });
876
885
  } catch (err) {
@@ -885,9 +894,9 @@ async function _switchScope(value) {
885
894
  }
886
895
 
887
896
  /**
888
- * "Set <ws> as focus": Local Dev's focus action (POST /api/workspaces
889
- * `focus`, which writes workspaces.yml and retargets the CLI). Separate
890
- * from the view: it is the only thing in the menu that moves the focus.
897
+ * Make `name` the CLI default: Local Dev's focus action (POST
898
+ * /api/workspaces `focus`, which writes workspaces.yml and retargets the
899
+ * CLI). False, with the error shown, when it couldn't.
891
900
  */
892
901
  async function _setFocus(name) {
893
902
  try {
@@ -900,9 +909,9 @@ async function _setFocus(name) {
900
909
  err.message = t("ws.focusFailed", { msg: err.message });
901
910
  window.denShell.errorToast(err);
902
911
  } else window.denShell?.toast?.(t("ws.focusFailed", { msg: err.message }));
903
- return;
912
+ return false;
904
913
  }
905
- location.reload();
914
+ return true;
906
915
  }
907
916
 
908
917
  // ── Health: the status chip and drawer (topbar.js, R6 and R15) ─────────────
@@ -29,7 +29,9 @@
29
29
  "ws.all": "Todos los activos",
30
30
  "ws.focus": "predeterminado de la CLI",
31
31
  "ws.focusTip":
32
- "Los comandos de la CLI se ejecutan aquí si no indicas un espacio de trabajo. Elegir un espacio en la barra superior solo cambia lo que muestran las páginas.",
32
+ "Los comandos de la CLI se ejecutan aquí si no indicas un espacio de trabajo. Elegir un espacio en la barra superior también lo convierte en el predeterminado de la CLI.",
33
+ "ws.pickTip":
34
+ "Elegir un espacio de trabajo aquí también lo convierte en el predeterminado de la CLI, así que los comandos de __DEN_CLI__ se ejecutan allí.",
33
35
  "ws.none": "No hay espacios de trabajo activos",
34
36
  "ws.manage": "Gestionar espacios de trabajo…",
35
37
  "ws.allN": "Todos los activos ({n})",
@@ -39,10 +41,7 @@
39
41
  "ws.viewTitle":
40
42
  "Vista: lo que muestra cada página ({ws}). Es solo un filtro: no inicia, detiene ni enruta nada.",
41
43
  "ws.viewTitleFocus":
42
- "Vista: lo que muestra cada página ({ws}). Es solo un filtro: no inicia, detiene ni enruta nada, y el predeterminado de la CLI ({focus}) sigue siendo el destino de los comandos.",
43
- "ws.setFocus": "Hacer de {ws} el predeterminado de la CLI",
44
- "ws.setFocusTip":
45
- "Lo convierte en el destino por defecto de los comandos (escribe workspaces.yml)",
44
+ "Vista: lo que muestra cada página ({ws}). No inicia, detiene ni enruta nada. Predeterminado de la CLI: {focus}.",
46
45
  "ws.viewFailed": "No se pudo cambiar la vista: {msg}",
47
46
  "ws.focusFailed": "No se pudo poner el foco: {msg}",
48
47
  "org.n": "{n} espacios de trabajo",
@@ -29,7 +29,9 @@
29
29
  "ws.all": "すべてのアクティブ",
30
30
  "ws.focus": "CLI の既定",
31
31
  "ws.focusTip":
32
- "ワークスペースを指定しないとき、CLI のコマンドはここで実行されます。上部バーでワークスペースを選んでも、ページの表示が変わるだけです。",
32
+ "ワークスペースを指定しないと CLI のコマンドはここで実行されます。上部バーでワークスペースを選ぶと、それが CLI の既定にもなります。",
33
+ "ws.pickTip":
34
+ "ここでワークスペースを選ぶと CLI の既定にもなり、__DEN_CLI__ のコマンドはそこで実行されます。",
33
35
  "ws.none": "アクティブなワークスペースはありません",
34
36
  "ws.manage": "ワークスペースを管理…",
35
37
  "ws.allN": "すべてのアクティブ ({n})",
@@ -39,10 +41,7 @@
39
41
  "ws.viewTitle":
40
42
  "表示: すべてのページに表示する範囲 ({ws})。表示の絞り込みだけで、何も起動・停止・ルーティングしません。",
41
43
  "ws.viewTitleFocus":
42
- "表示: すべてのページに表示する範囲 ({ws})。表示の絞り込みだけで、何も起動・停止・ルーティングしません。コマンドの対象は CLI の既定 ({focus}) のままです。",
43
- "ws.setFocus": "{ws} を CLI の既定にする",
44
- "ws.setFocusTip":
45
- "コマンドの既定の対象にします (workspaces.yml に書き込みます)",
44
+ "表示: 各ページに出るもの ({ws})。起動・停止・ルーティングは行いません。CLI の既定: {focus}。",
46
45
  "ws.viewFailed": "表示を変更できませんでした: {msg}",
47
46
  "ws.focusFailed": "フォーカスを設定できませんでした: {msg}",
48
47
  "org.n": "{n} 個のワークスペース",
@@ -1613,7 +1613,7 @@
1613
1613
  ? `<button class="danger" data-action="delete">Delete</button>`
1614
1614
  : "";
1615
1615
  return `<div class="ws-item ${n === activeWs ? "active" : ""}" data-name="${escHtml(n)}" data-action="select">
1616
- <span>${escHtml(n)}${isWsActive(n) ? ' <span style="font-size:10px;color:var(--muted)">(active)</span>' : ""}${n === focusName() ? ` <span class="ws-cli-default" title="${escHtml(wsT("ws.focusTip", "CLI commands run here when you don't name a workspace. Picking a workspace in the top bar only changes what pages show."))}">${escHtml(wsT("ws.focus", "CLI default"))}</span>` : ""}</span>
1616
+ <span>${escHtml(n)}${isWsActive(n) ? ' <span style="font-size:10px;color:var(--muted)">(active)</span>' : ""}${n === focusName() ? ` <span class="ws-cli-default" title="${escHtml(wsT("ws.focusTip", "CLI commands run here when you don't name a workspace. Picking a workspace in the top bar also makes it the CLI default."))}">${escHtml(wsT("ws.focus", "CLI default"))}</span>` : ""}</span>
1617
1617
  <span class="ws-item-right">
1618
1618
  <button class="ws-term-btn" title="Open in terminal" data-open-term="${escHtml(dirPath)}">&#62;_</button>
1619
1619
  <button class="ws-open-btn" title="Open in editor" data-open-path="${escHtml(wsData.codeWorkspacePaths?.[n] || dirPath)}">↗</button>
@@ -62,7 +62,7 @@
62
62
  * setOrgs([{ id, title, workspaces? }], currentId, onSelect?)
63
63
  * two or more: the Org ▾ menu; a pick
64
64
  * calls onSelect(id), fires `den:org`
65
- * setWorkspaces({ label, title, items, focusable, onSelect, onFocus })
65
+ * setWorkspaces({ label, title, items, focus, onSelect })
66
66
  * the Workspace ▾ menu (header.js)
67
67
  * syncEnv() repaint Env ▾ from #env-sel
68
68
  * registerCommand({ id, label, keywords?, kind?, run })
@@ -134,7 +134,9 @@
134
134
  "ws.all": "All active",
135
135
  "ws.focus": "CLI default",
136
136
  "ws.focusTip":
137
- "CLI commands run here when you don't name a workspace. Picking a workspace in the top bar only changes what pages show.",
137
+ "CLI commands run here when you don't name a workspace. Picking a workspace in the top bar also makes it the CLI default.",
138
+ "ws.pickTip":
139
+ "Picking a workspace here also makes it the CLI default, so __DEN_CLI__ commands run there.",
138
140
  "ws.none": "No active workspaces",
139
141
  "ws.manage": "Manage workspaces…",
140
142
  "ws.allN": "All active ({n})",
@@ -144,10 +146,7 @@
144
146
  "ws.viewTitle":
145
147
  "View: what every page shows ({ws}). A view filter only: it starts, stops and routes nothing.",
146
148
  "ws.viewTitleFocus":
147
- "View: what every page shows ({ws}). A view filter only: it starts, stops and routes nothing, and the CLI default ({focus}) stays the target for commands.",
148
- "ws.setFocus": "Make {ws} the CLI default",
149
- "ws.setFocusTip":
150
- "CLI commands run in it when you don't name a workspace (writes workspaces.yml)",
149
+ "View: what every page shows ({ws}). It starts, stops and routes nothing. CLI default: {focus}.",
151
150
  "ws.viewFailed": "Could not change the view: {msg}",
152
151
  "ws.focusFailed": "Could not change the CLI default: {msg}",
153
152
  // A count's singular is its .one (0.3.0-dev.40: en had none, so the org
@@ -1722,8 +1721,9 @@
1722
1721
  /**
1723
1722
  * The Workspace part: { label, title, items: [{ value, label, checked,
1724
1723
  * focused, title? }], onSelect(value) }. "All active" and the active
1725
- * workspaces, the focused one marked; then, apart, "Set <ws> as focus"
1726
- * (model.focusable, model.onFocus(name)) and a link to manage them.
1724
+ * workspaces, the focused one (model.focus, the CLI default) marked; then
1725
+ * a link to manage them. onSelect(value, focus): choosing a workspace
1726
+ * also makes it the CLI default (header.js).
1727
1727
  */
1728
1728
  function setWorkspaces(model) {
1729
1729
  var btn = document.getElementById("ws-pill");
@@ -1748,22 +1748,16 @@
1748
1748
  tag: it.focused ? t("ws.focus") : null,
1749
1749
  title: it.title || null,
1750
1750
  onSelect: function () {
1751
- if (!it.checked && typeof model.onSelect === "function")
1752
- model.onSelect(it.value);
1751
+ if (typeof model.onSelect !== "function") return;
1752
+ // The workspace in view still becomes the CLI default when it
1753
+ // isn't one; picking what is both already does nothing.
1754
+ if (it.checked && (it.value === "all" || it.value === model.focus))
1755
+ return;
1756
+ model.onSelect(it.value, model.focus || null, !!it.checked);
1753
1757
  },
1754
1758
  });
1755
1759
  });
1756
1760
  items.push({ hr: true });
1757
- // Focus is a separate action (it writes workspaces.yml and retargets
1758
- // the CLI); picking a view above never moves it.
1759
- if (model.focusable && typeof model.onFocus === "function")
1760
- items.push({
1761
- label: t("ws.setFocus", { ws: model.focusable }),
1762
- title: t("ws.setFocusTip"),
1763
- onSelect: function () {
1764
- model.onFocus(model.focusable);
1765
- },
1766
- });
1767
1761
  items.push({ label: t("ws.manage"), href: navHref("local-dev") });
1768
1762
  scopeMenus.ws = { label: t("k.ws"), items: items };
1769
1763
  }
@@ -25,7 +25,9 @@ export const EN_FALLBACK = {
25
25
  "ws.viewTitle":
26
26
  "View: what every page shows ({ws}). A view filter only: it starts, stops and routes nothing.",
27
27
  "ws.viewTitleFocus":
28
- "View: what every page shows ({ws}). A view filter only: it starts, stops and routes nothing, and the CLI default ({focus}) stays the target for commands.",
28
+ "View: what every page shows ({ws}). It starts, stops and routes nothing. CLI default: {focus}.",
29
+ "ws.pickTip":
30
+ "Picking a workspace here also makes it the CLI default, so __DEN_CLI__ commands run there.",
29
31
  };
30
32
  function enT(key, vars) {
31
33
  return String(EN_FALLBACK[key] ?? key).replace(/\{(\w+)\}/g, (m, k) =>
@@ -50,10 +52,10 @@ function focusName(data) {
50
52
  /**
51
53
  * The top bar's Workspace ▾ menu (R4, 0.3.0-dev.18), from the `/api/scope`
52
54
  * and `/api/workspaces` payloads. It is the view scope: what every page,
53
- * the health row and `status` show. Choosing an entry never moves the
54
- * focus (the default target for commands); the focused workspace is
55
- * marked, and `focusable` names the workspace in view when it is not the
56
- * focus, for the menu's separate "Set <ws> as focus" item.
55
+ * the health row and `status` show. Choosing a workspace also makes it the
56
+ * focus (the CLI default, Ward's decision in 0.4.11); "All active" leaves
57
+ * the focus where it is. The focused workspace is marked, and `focus`
58
+ * names it, so the caller can skip a choice that is already the focus.
57
59
  *
58
60
  * "All active" is offered with two or more active workspaces; with one,
59
61
  * it and that workspace are the same view.
@@ -85,12 +87,13 @@ export function buildScopeMenuModel(scope, data, t = enT) {
85
87
  label: n,
86
88
  checked: n === one,
87
89
  focused: n === focus,
90
+ title: t("ws.pickTip"),
88
91
  })),
89
92
  ];
90
93
  return {
91
94
  label: active.length === 0 ? t("ws.noneLabel") : (one ?? t("ws.all")),
92
95
  items,
93
- focusable: one && one !== focus ? one : null,
96
+ focus: focus ?? null,
94
97
  title: t(focus ? "ws.viewTitleFocus" : "ws.viewTitle", {
95
98
  ws: inView.join(", ") || t("ws.noneLabel"),
96
99
  focus,