create-pmos 0.88.0 → 0.89.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-pmos",
3
- "version": "0.88.0",
3
+ "version": "0.89.0",
4
4
  "description": "Scaffold the PMOS portable kit — run the PMOS product-management workflow (initiative → lane-fit spec → contract → build → three gates, with work-type lanes for features, research, maintenance, incidents, and content) entirely inside your own repo, with all state as git-versioned files. Includes `workflow-sweep`, which audits the state every board derives from and separates real defects from work you simply have not done. `create-pmos workspace` also installs + launches the local Terminal Workspace GUI. No database, no network, no telemetry.",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -1,3 +1,3 @@
1
- kernel-source: pmos@57df07b
1
+ kernel-source: pmos@5feb40c
2
2
  assembled: 2026-10-07
3
3
  mode: vendor
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Eval calibration log"
4
4
  description: "Append-only record of every PM override of an evaluator verdict."
5
- generated: {by: process:create-pmos, at: "2026-10-07T17:03:38Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-07T17:12:14Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Health budgets"
4
4
  description: "Closed registry of health-budget anchors (D59), edited only by the PM."
5
- generated: {by: process:create-pmos, at: "2026-10-07T17:03:38Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-07T17:12:14Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Discoveries and friction log"
4
4
  description: "Append-only record of what runs discovered and where the process rubbed."
5
- generated: {by: process:create-pmos, at: "2026-10-07T17:03:38Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-07T17:12:14Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -57,9 +57,9 @@ The dock holds your terminals, documents, conversations, skills and web pages. I
57
57
  - Press **⋯** on a row to change the name, the initiative or the type of work.
58
58
  - Press **✕** on a row to end the session. The row asks "End “name”?". Press **End** or **Cancel**. When you end the last running session on its own working copy, the message offers **Remove working copy**. This removes the folder only. The branch stays. If the folder has unsaved work, the workspace keeps it and tells you why.
59
59
  - **+ Session** at the top of the dock, or **+** on an initiative, opens one sheet. Type a name, or leave it empty to name the session after the skill and the initiative. **For** is the initiative. **On** is where the session runs. **Automatic** uses the initiative's own working copy on `initiative/<id>` (made the first time), or the main tree when there is no initiative. You can also choose a branch that has a working copy, or **New branch…**. Then press **Blank session** (the CLI and model from Settings) or a skill. The session starts at once.
60
- - The icons at the top of the session list, under **+ Session**, show the sessions as a strip on top, give the dock the whole window (Focus), and fold the dock. They stay in view when you scroll the list. On top, a select chooses the group, and each session of the group is a button. **+** starts a session, and **✕** ends the session in front.
60
+ - The icons at the top of the session list, under **+ Session**, switch the sessions as a strip on top or a list at the side, set the dock size, and fold the dock. They stay in view when you scroll the list. On top, a select chooses the group, and each session of the group is a button. **+** starts a session, and **✕** ends the session in front.
61
61
  - To make the dock wider or narrower, drag its left edge. With the edge focused, the left and right arrow keys change the width in steps. Double-click the edge to go back to the default width. The width is kept in this browser.
62
- - **Focus** in the dock gives the dock the whole window. The place and the detail are hidden, and the dock stays full width on every place. Press Focus again, or ⌘⇧J (Ctrl-Shift-J), to show the place again. Folding the dock also ends Focus.
62
+ - **Dock size** opens a menu of four sizes: Normal (the default width), Half the window, Two-thirds, and Whole window (focus). Choose one to set the dock to that size. The size is remembered in this browser. Dragging the edge clears the chosen size and sets a pixel width instead. Whole window (focus) gives the dock the entire screen; the place and the detail are hidden, and the dock stays full width on every place. Press Whole window again, or ⌘⇧J (Ctrl-Shift-J), to show the place. Folding the dock also ends focus.
63
63
  - **Scratchpad** in the bar at the bottom of the dock opens and closes the scratchpad. The pad keeps one note for each terminal session, and one for the repository when no terminal is in front. **Send to terminal** types the note into the session in front and presses Enter. It is available only when a terminal is in front. **Copy** and **Clear** act on the note.
64
64
  - The same bottom bar shows the title of the tab in front and a copy button that names what it copies: **Copy document** for a document, **Copy attach command** for a terminal.
65
65
  - A document tab has one bar. The words at its start say whether the file is saved on disk or has unsaved changes. The copy button beside them shows the file's Confluence or Google Docs copy (see [Edit a document in place](#edit-a-document-in-place)). **Changes**, **Revert** and **Save** show when there are unsaved changes. **Markdown** shows the whole file as markdown in one box; press it again to go back. A file that you cannot edit has **Source** in its place. **⋯** holds the rest: the reading width (full width, or lines of 72 characters), **Normalize line breaks** (it joins hard-wrapped paragraphs into one line each in the buffer; read the result in Changes, then save) and **Hide the outline and connections**. **Run scripts** shows on the bar for an HTML page that can run its own scripts.
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.88.0",
3
+ "version": "0.89.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pmos-workspace",
9
- "version": "0.88.0",
9
+ "version": "0.89.0",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@xterm/addon-fit": "^0.11.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.88.0",
3
+ "version": "0.89.0",
4
4
  "private": true,
5
5
  "description": "PMOS Terminal Workspace — local companion hosting real PTYs beside the library and gates (D62)",
6
6
  "license": "MIT",
@@ -414,7 +414,7 @@ function readFrame() {
414
414
  }
415
415
  const fr = (() => {
416
416
  const s = readFrame();
417
- return { dest: FRAME_DESTS.includes(s.dest) ? s.dest : 'today', scope: s.scope === 'all' ? 'all' : 'repo', params: {}, dock: {}, dockMax: false, dockW: null, syncing: false, reg: null, gAt: 0, needs: 0 };
417
+ return { dest: FRAME_DESTS.includes(s.dest) ? s.dest : 'today', scope: s.scope === 'all' ? 'all' : 'repo', params: {}, dock: {}, dockMax: false, dockW: null, dockSize: null, syncing: false, reg: null, gAt: 0, needs: 0 };
418
418
  })();
419
419
  function writeFrame() {
420
420
  try { localStorage.setItem(FRAME_KEY, JSON.stringify({ dest: fr.dest, scope: fr.scope })); } catch { /* private mode: the default stands */ }
@@ -444,6 +444,34 @@ function toggleFocus() {
444
444
  if (fr.dockMax) fr.dock[fr.dest] = true;
445
445
  setState({});
446
446
  }
447
+ /* Dock size: set the dock to one of four preset sizes, or to focus mode.
448
+ * Normal: default width (no CSS override). Half: 50vw. Two-thirds: 66.667vw (the body spans the window).
449
+ * Focus: the dock takes the whole window (same as toggleFocus). */
450
+ const DOCK_SIZE_KEY = 'pmos.workspace.pm.dock-size';
451
+ const DOCK_SIZES = ['normal', 'half', 'two-thirds'];
452
+ function readDockSize() {
453
+ if (!FRAME) return null;
454
+ let v = null;
455
+ try { v = localStorage.getItem(DOCK_SIZE_KEY) || null; } catch { v = null; }
456
+ return v === 'half' || v === 'two-thirds' ? v : null;
457
+ }
458
+ function setDockSize(next) {
459
+ if (!FRAME) return;
460
+ if (next === 'whole') {
461
+ if (!fr.dockMax) toggleFocus();
462
+ return;
463
+ }
464
+ if (!DOCK_SIZES.includes(next)) return;
465
+ // A preset replaces a dragged pixel width, and leaving focus returns to the size chosen here.
466
+ fr.dockW = null;
467
+ try { localStorage.removeItem(DOCK_W_KEY); } catch { /* private mode */ }
468
+ fr.dockSize = next === 'normal' ? null : next;
469
+ try { if (fr.dockSize) localStorage.setItem(DOCK_SIZE_KEY, fr.dockSize); else localStorage.removeItem(DOCK_SIZE_KEY); } catch { /* private mode */ }
470
+ fr.dockMax = false;
471
+ fr.dock[fr.dest] = true;
472
+ paintDockWidth();
473
+ setState({});
474
+ }
447
475
  /* The dock's width, chosen with the grip (0.53.1). Kept in this browser like the dock's other per-window choices;
448
476
  * null is the default width. */
449
477
  const DOCK_W_KEY = 'pmos.workspace.pm.dock-width';
@@ -457,12 +485,21 @@ const dockWidth = {
457
485
  get: () => fr.dockW,
458
486
  set: (px) => {
459
487
  fr.dockW = Number.isFinite(px) && px > 0 ? Math.round(px) : null;
488
+ fr.dockSize = null; // dragging the grip, or a double-click, replaces a preset
460
489
  try { if (fr.dockW) localStorage.setItem(DOCK_W_KEY, String(fr.dockW)); else localStorage.removeItem(DOCK_W_KEY); } catch { /* private mode */ }
490
+ if (!fr.dockSize) try { localStorage.removeItem(DOCK_SIZE_KEY); } catch { /* private mode */ }
461
491
  paintDockWidth();
462
492
  },
463
493
  };
464
494
  function paintDockWidth() {
465
- const v = fr.dockW ? fr.dockW + 'px' : '';
495
+ let v = '';
496
+ if (fr.dockW) {
497
+ v = fr.dockW + 'px';
498
+ } else if (fr.dockSize === 'half') {
499
+ v = '50vw';
500
+ } else if (fr.dockSize === 'two-thirds') {
501
+ v = '66.667vw';
502
+ }
466
503
  if (bodyGrid.style.getPropertyValue('--fr-dock-w') !== v) {
467
504
  if (v) bodyGrid.style.setProperty('--fr-dock-w', v); else bodyGrid.style.removeProperty('--fr-dock-w');
468
505
  }
@@ -574,6 +611,7 @@ function setupFrame() {
574
611
  document.addEventListener('pointerdown', (e) => { fr.downAt = e.target; }, true);
575
612
  dockRail = createDockRail({ open: openDock, resize: dockWidth });
576
613
  fr.dockW = readDockW();
614
+ fr.dockSize = readDockSize();
577
615
  frDetail = el('div', { class: 'fr-detail', 'data-fr': 'detail' },
578
616
  el('div', { class: 'fr-detail__bar' }, iconButton({ icon: 'x', ariaLabel: FC.detail.close, onClick: closeDetail, attrs: { 'data-fr': 'detail-close' } })),
579
617
  hostDetail);
@@ -712,9 +750,9 @@ function frameKey(e) {
712
750
 
713
751
  /* What the suites and the phone chrome reach the frame by. */
714
752
  const frameApi = {
715
- on: true, go, toggleDock, foldDock, openDock, toggleFocus, dockWidth, chooseRepo, chooseAll, openPalette, repoMenu, openBranchesSheet, closeDetail, sync: runSync,
753
+ on: true, go, toggleDock, foldDock, openDock, toggleFocus, setDockSize, dockWidth, chooseRepo, chooseAll, openPalette, repoMenu, openBranchesSheet, closeDetail, sync: runSync,
716
754
  get unsynced() { return !!fr.unsynced; },
717
- get dest() { return fr.dest; }, get scope() { return fr.scope; }, get dockOpen() { return dockOpen(); }, get dockMax() { return !!fr.dockMax; },
755
+ get dest() { return fr.dest; }, get scope() { return fr.scope; }, get dockOpen() { return dockOpen(); }, get dockMax() { return !!fr.dockMax; }, get dockSize() { return fr.dockSize || 'normal'; },
718
756
  get detailOpen() { return !!state.col2; }, get syncing() { return fr.syncing; }, get needs() { return fr.needs || 0; },
719
757
  };
720
758
 
@@ -652,6 +652,16 @@
652
652
  white-space: nowrap;
653
653
  cursor: pointer;
654
654
  }
655
+ /* The shortcut hint sits at a row's end; a checked row (a menuitemradio) is marked. */
656
+ .pmc-menu__hint {
657
+ margin-left: auto;
658
+ padding-left: 16px;
659
+ color: var(--muted-foreground);
660
+ font: 11px var(--mono);
661
+ }
662
+ .pmc-menu__item[aria-checked="true"] {
663
+ background: var(--muted);
664
+ }
655
665
  .pmc-menu__item--danger {
656
666
  color: var(--destructive);
657
667
  }
@@ -9,8 +9,8 @@
9
9
  * createDockRail({ open, resize }) the 44px rail: one control that opens the dock. While the dock is open the rail
10
10
  * is a thin grip on the dock's left edge that resizes it (0.53.1)
11
11
  * foldButton({ fold }) the control in the open dock's strip that folds it back to the rail
12
- * focusButton({ toggle }) / paintFocus(b, on)
13
- * Focus: the dock takes the whole window, and Exit focus gives the page back (0.53.1)
12
+ * dockSizeButton({ setSize, getState }) / paintDockSize(b, size, inFocus)
13
+ * Dock size menu: Normal, Half, Two-thirds, Whole window (focus; 0.53.1, folded into the menu later)
14
14
  * sessionMenuButton({ open, ws }) "+ Session": opens the new-session sheet (dock-sessions.js): a blank
15
15
  * session or a skill to start from (R10; one list since 0.55.1)
16
16
  * sessionDefaults() / setSessionDefaults(patch)
@@ -23,7 +23,7 @@
23
23
  import { el } from './dom.js';
24
24
  import { FIXTURE } from './state.js';
25
25
  import { pmGet } from './http.js';
26
- import { button, iconButton } from './primitives.js';
26
+ import { button, iconButton, menu } from './primitives.js';
27
27
  import { icon } from './icons.js';
28
28
  import { COPY } from './frame-copy.js';
29
29
 
@@ -103,19 +103,59 @@ const endDrag = () => { if (drag) { drag.g.removeAttribute('data-dragging'); dra
103
103
  window.addEventListener('pointerup', endDrag);
104
104
  window.addEventListener('pointercancel', endDrag);
105
105
 
106
- /* 0.55.1: icon only (the PM asked for no label): maximize-2 to focus, minimize-2 to leave; the name is the aria-label. */
107
- export function focusButton({ toggle } = {}) {
108
- return iconButton({ icon: 'maximize-2', ariaLabel: COPY.dock.focusTitle, onClick: () => toggle && toggle(),
109
- attrs: { class: 'fr-dock-focus', 'data-fr': 'dock-focus', 'aria-pressed': 'false', 'aria-keyshortcuts': 'Meta+Shift+J Control+Shift+J' } });
106
+ /* Dock size menu: Normal, Half, Two-thirds, or Whole window (focus). It replaces the old Focus button; focus is its last row
107
+ * and keeps ⌘⇧J. The button shows the current size; the menu is built on each press, so it always shows the current choice. */
108
+ const SVG_NS = 'http://www.w3.org/2000/svg';
109
+ const SIZE_SHARE = { normal: 7, half: 9, 'two-thirds': 12 }; // width of the filled part of the glyph, out of 18
110
+ /** A small window glyph with the dock's share filled on the right. Own SVG: the vendored sprite has no such symbol. */
111
+ function sizeGlyph(size) {
112
+ const svg = icon('square'); // the primitive's own <svg> (class, size, aria-hidden); its symbol is replaced below
113
+ svg.replaceChildren();
114
+ svg.setAttribute('viewBox', '0 0 24 24');
115
+ svg.setAttribute('fill', 'none');
116
+ svg.setAttribute('stroke', 'currentColor');
117
+ svg.setAttribute('stroke-width', '2');
118
+ svg.setAttribute('data-icon', 'dock-' + size);
119
+ const frame = document.createElementNS(SVG_NS, 'rect');
120
+ for (const [k, v] of Object.entries({ x: 3, y: 5, width: 18, height: 14, rx: 2 })) frame.setAttribute(k, String(v));
121
+ const fill = document.createElementNS(SVG_NS, 'rect');
122
+ const w = SIZE_SHARE[size] || SIZE_SHARE.normal;
123
+ for (const [k, v] of Object.entries({ x: 21 - w, y: 5, width: w, height: 14 })) fill.setAttribute(k, String(v));
124
+ fill.setAttribute('fill', 'currentColor');
125
+ fill.setAttribute('stroke', 'none');
126
+ svg.append(frame, fill);
127
+ return svg;
110
128
  }
111
- /** Show the Focus control's state: pressed while the dock has the whole window. */
112
- export function paintFocus(b, on) {
129
+ const sizeNode = (size, inFocus) => (inFocus ? icon('minimize-2') : sizeGlyph(size || 'normal'));
130
+
131
+ export function dockSizeButton({ setSize, getState } = {}) {
132
+ return iconButton({ icon: 'maximize-2', ariaLabel: COPY.dock.dockSize,
133
+ attrs: { class: 'fr-dock-size', 'data-fr': 'dock-size', 'aria-haspopup': 'menu', 'aria-expanded': 'false', 'aria-keyshortcuts': 'Meta+Shift+J Control+Shift+J' },
134
+ onClick: (e) => {
135
+ const b = e.currentTarget;
136
+ const st = (getState && getState()) || {};
137
+ const cur = st.inFocus ? 'whole' : (st.size || 'normal');
138
+ const row = (id, label, hint, node) => ({
139
+ label, iconNode: node, hint, role: 'menuitemradio',
140
+ attrs: { 'data-size': id, 'aria-checked': cur === id ? 'true' : 'false' },
141
+ onSelect: () => setSize && setSize(id),
142
+ });
143
+ menu({ anchor: b, ariaLabel: COPY.dock.dockSizeMenu, items: [
144
+ row('normal', COPY.dock.sizeNormal, '', sizeGlyph('normal')),
145
+ row('half', COPY.dock.sizeHalf, '', sizeGlyph('half')),
146
+ row('two-thirds', COPY.dock.sizeTwoThirds, '', sizeGlyph('two-thirds')),
147
+ row('whole', COPY.dock.sizeWhole, '⌘⇧J', icon('maximize-2')),
148
+ ] });
149
+ } });
150
+ }
151
+
152
+ /** Show the button's glyph and name for the current size. */
153
+ export function paintDockSize(b, size, inFocus) {
113
154
  if (!b) return;
114
- const name = on ? 'minimize-2' : 'maximize-2';
155
+ const want = inFocus ? 'minimize-2' : 'dock-' + (size || 'normal');
115
156
  const cur = b.querySelector('[data-icon]');
116
- if (!cur || cur.getAttribute('data-icon') !== name) b.replaceChildren(icon(name));
117
- b.setAttribute('aria-pressed', on ? 'true' : 'false');
118
- const label = on ? COPY.dock.unfocusTitle : COPY.dock.focusTitle;
157
+ if (!cur || cur.getAttribute('data-icon') !== want) b.replaceChildren(sizeNode(size, inFocus));
158
+ const label = inFocus ? COPY.dock.unfocusTitle : COPY.dock.dockSize;
119
159
  b.setAttribute('aria-label', label);
120
160
  b.title = label;
121
161
  }
@@ -62,6 +62,12 @@ export const COPY = {
62
62
  unfocus: 'Exit focus',
63
63
  focusTitle: 'Give the dock the whole window (⌘⇧J)',
64
64
  unfocusTitle: 'Show the page beside the dock again (⌘⇧J)',
65
+ dockSize: 'Dock size',
66
+ dockSizeMenu: 'Choose dock size',
67
+ sizeNormal: 'Normal',
68
+ sizeHalf: 'Half the window',
69
+ sizeTwoThirds: 'Two-thirds',
70
+ sizeWhole: 'Whole window (focus)',
65
71
  resize: 'Resize the agent dock. Drag, or use the left and right arrow keys. Double-click resets the width.',
66
72
  session: 'Session',
67
73
  sessionMenu: 'Start a session',
@@ -1152,11 +1152,14 @@ function frameDock() {
1152
1152
  open: () => (dsMod ? dsMod.openNewSessionSheet(currentProduct(), { initiative: null }) : newSession()),
1153
1153
  }),
1154
1154
  fold: dockParts ? dockParts.fold : dockMod.foldButton({ fold: () => window.pmApp.frame.foldDock() }),
1155
- focus: dockParts ? dockParts.focus : dockMod.focusButton({ toggle: () => window.pmApp.frame.toggleFocus() }),
1155
+ size: dockParts ? dockParts.size : dockMod.dockSizeButton({
1156
+ setSize: (sz) => window.pmApp.frame.setDockSize(sz),
1157
+ getState: () => ({ size: window.pmApp.frame.dockSize, inFocus: !!window.pmApp.frame.dockMax }),
1158
+ }),
1156
1159
  };
1157
1160
  }
1158
- if (isMobile()) return { session: dockParts.session, fold: null, focus: null, orient: null };
1159
- dockMod.paintFocus(dockParts.focus, !!window.pmApp.frame.dockMax);
1161
+ if (isMobile()) return { session: dockParts.session, fold: null, size: null, orient: null };
1162
+ dockMod.paintDockSize(dockParts.size, window.pmApp.frame.dockSize, !!window.pmApp.frame.dockMax);
1160
1163
  // the tabs-on-top / tabs-at-side switch, icon only in the frame (0.55.1); drawn per paint because its icon follows the mode
1161
1164
  // one element across paints, so the keyboard focus stays on it; only its icon and name follow the mode
1162
1165
  const v = !!state.tabsVertical;
@@ -1170,7 +1173,7 @@ function frameDock() {
1170
1173
  const olabel = v ? COPY.dock.topTabs : COPY.dock.sideTabs;
1171
1174
  dockOrient.setAttribute('aria-label', olabel);
1172
1175
  dockOrient.title = olabel;
1173
- return { session: dockParts.session, fold: dockParts.fold, focus: dockParts.focus, orient: dockOrient };
1176
+ return { session: dockParts.session, fold: dockParts.fold, size: dockParts.size, orient: dockOrient };
1174
1177
  }
1175
1178
 
1176
1179
  /**
@@ -1395,7 +1398,7 @@ export function renderTabs(host) {
1395
1398
  newSessionOptsBtn,
1396
1399
  canSplit ? splitBtn : null,
1397
1400
  orientBtn,
1398
- fr ? fr.focus : null,
1401
+ fr ? fr.size : null,
1399
1402
  fr ? fr.fold : null),
1400
1403
  ]);
1401
1404
  } else if (ck) {
@@ -1410,7 +1413,7 @@ export function renderTabs(host) {
1410
1413
  ].filter(Boolean);
1411
1414
  extras[0].style.marginLeft = 'auto';
1412
1415
  // The switch sits after `+ session`, so the strip still ends the mockup's way in the fixture, which does not draw it.
1413
- setChildren(tabBar, [...visTabs.map((t) => cockpitTabNode(t, act, splitTab)), ...extras, fr ? fr.session : newSessionBtn, FIXTURE ? null : orientBtn, fr ? fr.focus : null, fr ? fr.fold : null]);
1416
+ setChildren(tabBar, [...visTabs.map((t) => cockpitTabNode(t, act, splitTab)), ...extras, fr ? fr.session : newSessionBtn, FIXTURE ? null : orientBtn, fr ? fr.size : null, fr ? fr.fold : null]);
1414
1417
  } else {
1415
1418
  tabBar.setAttribute('style', BAR_STYLE);
1416
1419
  newSessionBtn.setAttribute('style', CHIP_STYLE);
@@ -1437,11 +1440,11 @@ export function renderTabs(host) {
1437
1440
  ...dsMod.railNodes(ds, others.map((t) => dockTabRow(t, (act && act.id === t.id) || (splitTab && splitTab.id === t.id)))),
1438
1441
  ] : [
1439
1442
  ...dsMod.stripNodes(ds, others.map((t) => dockTabChip(t, (act && act.id === t.id) || (splitTab && splitTab.id === t.id)))),
1440
- state.splitId ? splitBtn : null, fr.orient, fr.focus, fr.fold,
1443
+ state.splitId ? splitBtn : null, fr.orient, fr.size, fr.fold,
1441
1444
  ]);
1442
1445
  }
1443
1446
 
1444
- if (ck && ds && vertical) setChildren(dockHead, [fr.session, state.splitId ? splitBtn : null, fr.orient, fr.focus, fr.fold]);
1447
+ if (ck && ds && vertical) setChildren(dockHead, [fr.session, state.splitId ? splitBtn : null, fr.orient, fr.size, fr.fold]);
1445
1448
  if (tabBar.scrollLeft !== keepX) tabBar.scrollLeft = keepX;
1446
1449
  if (tabBar.scrollTop !== keepY) tabBar.scrollTop = keepY;
1447
1450
  // When the tab in front changes, the bar scrolls just enough to show it, so a tab opened at the far end of a crowded strip or a
@@ -354,9 +354,9 @@ export function menu({ anchor, items, ariaLabel } = {}) {
354
354
  if (it.sep) { node.appendChild(el('div', { class: 'pmc-menu__sep', role: 'separator' })); continue; }
355
355
  const b = el('button', {
356
356
  ...(it.attrs || {}),
357
- type: 'button', role: 'menuitem', tabindex: '-1', class: cx('pmc-menu__item', it.danger && 'pmc-menu__item--danger'),
357
+ type: 'button', role: it.role || 'menuitem', tabindex: '-1', class: cx('pmc-menu__item', it.danger && 'pmc-menu__item--danger'),
358
358
  onClick: (e) => { e.stopPropagation(); close(false); if (it.onSelect) it.onSelect(); },
359
- }, it.icon ? icon(it.icon) : null, el('span', null, it.label));
359
+ }, it.iconNode || (it.icon ? icon(it.icon) : null), el('span', null, it.label), it.hint ? el('kbd', { class: 'pmc-menu__hint' }, it.hint) : null);
360
360
  btns.push(b); node.appendChild(b);
361
361
  }
362
362
  node.addEventListener('toggle', (e) => { if (e.newState === 'closed') close(false); });