create-pmos 0.52.0 → 0.53.1
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 +1 -1
- package/payload/KIT-VERSION +2 -2
- package/payload/mods/pmos-cockpit/README.md +13 -0
- package/payload/mods/pmos-cockpit/hooks/lib/commands.mjs +2 -1
- package/payload/mods/pmos-cockpit/hooks/lib/grants.mjs +41 -11
- package/payload/mods/pmos-cockpit/hooks/lib/guard.mjs +228 -8
- package/payload/mods/pmos-cockpit/hooks/register.tsx +11 -3
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/scripts/plan-items-stage.js +271 -0
- package/payload/scripts/pmos-log.sh +18 -7
- package/payload/scripts/workflow-state.js +28 -2
- package/payload/state/discoveries.md +1 -1
- package/payload/state/log.md +1 -1
- package/workspace/README.md +2 -0
- package/workspace/REMOTE.md +24 -63
- package/workspace/bin/pmos.js +19 -3
- package/workspace/cut-stub.js +40 -0
- package/workspace/first-paint-audit.js +145 -0
- package/workspace/fixtures/capture/requirements/capture-2026-10-04-1.md +10 -0
- package/workspace/fixtures/initiatives-api/list.json +176 -0
- package/workspace/fixtures/initiatives-api/post-403.json +5 -0
- package/workspace/fixtures/initiatives-api/post-ok.json +10 -0
- package/workspace/fixtures/initiatives-api/roadmap.json +208 -0
- package/workspace/fixtures/initiatives-api/sheet.json +112 -0
- package/workspace/fixtures/report-api/draft.json +177 -0
- package/workspace/fixtures/reports-page/draft-biweekly-2026-10-16.md +97 -0
- package/workspace/fixtures/today-api/provider-rows.json +204 -0
- package/workspace/fixtures/today-api/seam-probe-kinds.json +5 -0
- package/workspace/guide/board.md +81 -0
- package/workspace/guide/breakdown.md +154 -0
- package/workspace/guide/command-palette.md +75 -0
- package/workspace/guide/index.md +28 -17
- package/workspace/guide/initiatives-roadmap.md +130 -0
- package/workspace/guide/initiatives-screen.md +89 -0
- package/workspace/guide/initiatives-timeline.md +57 -0
- package/workspace/guide/integrated-work.md +94 -0
- package/workspace/guide/knowledge-graph.md +3 -68
- package/workspace/guide/launcher.md +51 -0
- package/workspace/guide/layout.md +102 -32
- package/workspace/guide/live-notes.md +3 -3
- package/workspace/guide/pm-only-acts.md +89 -0
- package/workspace/guide/pm-only-lines.md +61 -0
- package/workspace/guide/proactive-engine.md +21 -5
- package/workspace/guide/reports-page.md +80 -0
- package/workspace/guide/reports.md +73 -0
- package/workspace/guide/timeline.md +70 -0
- package/workspace/guide/today.md +85 -0
- package/workspace/guide/validate-with-typesafe.md +1 -1
- package/workspace/lib/actor.js +91 -0
- package/workspace/lib/board-cache.js +32 -2
- package/workspace/lib/board-cli.js +108 -0
- package/workspace/lib/board-fold.js +232 -0
- package/workspace/lib/breakdown-export.js +75 -0
- package/workspace/lib/brief.js +20 -2
- package/workspace/lib/capture.js +98 -0
- package/workspace/lib/engine/plan-items-stage.js +271 -0
- package/workspace/lib/engine/workflow-state.js +28 -2
- package/workspace/lib/factory-join.js +158 -0
- package/workspace/lib/goals.js +165 -0
- package/workspace/lib/grant-mint.js +113 -0
- package/workspace/lib/health-rules-factory.js +92 -0
- package/workspace/lib/health-rules-plan.js +164 -0
- package/workspace/lib/health.js +79 -0
- package/workspace/lib/initiative-cli.js +222 -0
- package/workspace/lib/initiative-display.js +183 -0
- package/workspace/lib/initiative-needs.js +127 -0
- package/workspace/lib/initiative-rows.js +210 -0
- package/workspace/lib/initiative-writer.js +141 -0
- package/workspace/lib/issues.js +206 -0
- package/workspace/lib/item-cli.js +195 -0
- package/workspace/lib/item-spans.js +137 -0
- package/workspace/lib/launcher-cli.js +207 -0
- package/workspace/lib/measure-cli.js +100 -0
- package/workspace/lib/measurements.js +189 -0
- package/workspace/lib/mobile-nav.js +57 -53
- package/workspace/lib/needs-providers.js +120 -0
- package/workspace/lib/placements.js +440 -0
- package/workspace/lib/plan-items.js +922 -0
- package/workspace/lib/plan-ledgers.js +85 -0
- package/workspace/lib/plan-rules.js +72 -0
- package/workspace/lib/plan-tree.js +380 -0
- package/workspace/lib/pm-guard.js +174 -0
- package/workspace/lib/pm-lines-needs.js +331 -0
- package/workspace/lib/pmos-verbs.js +69 -0
- package/workspace/lib/proactive-cli.js +58 -2
- package/workspace/lib/proactive.js +9 -6
- package/workspace/lib/qr.js +293 -0
- package/workspace/lib/remote-access.js +93 -1
- package/workspace/lib/report-cli.js +152 -0
- package/workspace/lib/report-draft.js +578 -0
- package/workspace/lib/report-needs.js +213 -0
- package/workspace/lib/route-modules.js +128 -0
- package/workspace/lib/routes-board.js +91 -0
- package/workspace/lib/routes-capture.js +52 -0
- package/workspace/lib/routes-initiatives.js +371 -0
- package/workspace/lib/routes-reports.js +163 -0
- package/workspace/lib/routes-timeline.js +197 -0
- package/workspace/lib/routes-today.js +32 -0
- package/workspace/lib/today-cli.js +99 -0
- package/workspace/lib/today.js +333 -0
- package/workspace/lib/triage.js +163 -0
- package/workspace/lib/verify-work-cli.js +81 -0
- package/workspace/lib/work-check-needs.js +99 -0
- package/workspace/package-lock.json +1 -1
- package/workspace/package.json +4 -5
- package/workspace/public/icons/apple-touch-icon.png +0 -0
- package/workspace/public/icons/icon-192.png +0 -0
- package/workspace/public/icons/icon-512.png +0 -0
- package/workspace/public/index.html +40 -19
- package/workspace/public/pm/app.js +330 -30
- package/workspace/public/pm/bd-board.js +113 -0
- package/workspace/public/pm/bd-drag.js +59 -0
- package/workspace/public/pm/bd-model.js +152 -0
- package/workspace/public/pm/bd-timeline.js +233 -0
- package/workspace/public/pm/bd-tree.js +244 -0
- package/workspace/public/pm/board-copy.js +57 -0
- package/workspace/public/pm/breakdown-api.js +58 -0
- package/workspace/public/pm/breakdown-copy.js +138 -0
- package/workspace/public/pm/components.css +3135 -0
- package/workspace/public/pm/cycle-review.js +125 -0
- package/workspace/public/pm/data.js +40 -187
- package/workspace/public/pm/dest-board.js +438 -0
- package/workspace/public/pm/dest-initiatives.js +134 -0
- package/workspace/public/pm/dest-reports.js +270 -0
- package/workspace/public/pm/dest-settings.js +217 -0
- package/workspace/public/pm/dest-today.js +182 -0
- package/workspace/public/pm/dialogs.js +88 -0
- package/workspace/public/pm/display-word.js +56 -0
- package/workspace/public/pm/dock.js +175 -0
- package/workspace/public/pm/fixture.js +8 -227
- package/workspace/public/pm/frame-copy.js +99 -0
- package/workspace/public/pm/handoff-box.js +82 -0
- package/workspace/public/pm/http.js +88 -0
- package/workspace/public/pm/icons.js +53 -0
- package/workspace/public/pm/initiative-list.js +218 -0
- package/workspace/public/pm/initiative-sheet.js +261 -0
- package/workspace/public/pm/initiatives-api.js +59 -0
- package/workspace/public/pm/initiatives-copy.js +215 -0
- package/workspace/public/pm/initiatives-timeline-copy.js +59 -0
- package/workspace/public/pm/initiatives-timeline.js +410 -0
- package/workspace/public/pm/initiatives-ui.js +123 -0
- package/workspace/public/pm/item-sheet.js +251 -0
- package/workspace/public/pm/keep-card.js +204 -0
- package/workspace/public/pm/lifecycle.js +7 -1
- package/workspace/public/pm/mobile.css +49 -20
- package/workspace/public/pm/mobile.js +157 -29
- package/workspace/public/pm/modes-live.js +7 -883
- package/workspace/public/pm/modes.js +32 -1024
- package/workspace/public/pm/new-initiative-sheet.js +74 -0
- package/workspace/public/pm/next-step-ui.js +3 -2
- package/workspace/public/pm/palette-copy.js +51 -0
- package/workspace/public/pm/palette.js +312 -0
- package/workspace/public/pm/panel-breakdown.js +702 -0
- package/workspace/public/pm/panel-cockpit.js +63 -295
- package/workspace/public/pm/panel-detail.js +25 -172
- package/workspace/public/pm/panel-initiatives.js +1 -19
- package/workspace/public/pm/panel-repos.js +31 -81
- package/workspace/public/pm/panel-tabs.js +200 -111
- package/workspace/public/pm/panel-trees.js +5 -2
- package/workspace/public/pm/portfolio-report.js +61 -0
- package/workspace/public/pm/primitives.js +656 -0
- package/workspace/public/pm/print.css +33 -0
- package/workspace/public/pm/provider-cards.js +136 -0
- package/workspace/public/pm/registry.js +194 -0
- package/workspace/public/pm/release-card.js +110 -0
- package/workspace/public/pm/report-view.js +309 -0
- package/workspace/public/pm/reports-copy.js +114 -0
- package/workspace/public/pm/roadmap.js +271 -0
- package/workspace/public/pm/sig-watch.js +41 -0
- package/workspace/public/pm/state.js +8 -4
- package/workspace/public/pm/status-banner.js +13 -5
- package/workspace/public/pm/tab-doc.js +2 -4
- package/workspace/public/pm/tab-term.js +12 -2
- package/workspace/public/pm/tab-web.js +3 -2
- package/workspace/public/pm/timeline-copy.js +48 -0
- package/workspace/public/pm/today-cards.js +244 -0
- package/workspace/public/pm/today-copy.js +126 -0
- package/workspace/public/pm/today-goals.js +172 -0
- package/workspace/public/pm/tokens.css +16 -18
- package/workspace/public/pm/topbar.js +102 -0
- package/workspace/public/pm/triage-card.js +60 -0
- package/workspace/public/pm/undo-hold.js +103 -0
- package/workspace/public/pm/view-addrepo.js +2 -1
- package/workspace/public/pm/view-livenotes.js +3 -1
- package/workspace/public/pm/view-today.js +7 -162
- package/workspace/public/vendor/Sortable.min.js +2 -0
- package/workspace/public/vendor/lucide-sprite.js +79 -0
- package/workspace/replaced-tests.json +2877 -0
- package/workspace/run-tests.js +56 -5
- package/workspace/server.js +446 -40
- package/workspace/sheet-answer.js +61 -0
- package/workspace/skills/measure.skill +41 -0
- package/workspace/skills/morning-brief.skill +15 -9
- package/workspace/skills/report-biweekly.skill +23 -25
- package/workspace/skills/tracker-refresh.skill +50 -0
- package/workspace/test-files.json +21 -3
- package/workspace/test-lists/base.json +6 -0
- package/workspace/test-lists/run2-a.json +6 -0
- package/workspace/test-lists/run2-b.json +13 -0
- package/workspace/test-lists/run2-c.json +19 -0
- package/workspace/test-lists/run2-d.json +22 -0
- package/workspace/test-lists/run3-0.json +5 -0
- package/workspace/test-lists/run3-a.json +5 -0
- package/workspace/test-lists/run3-b.json +6 -0
- package/workspace/test-lists/run3-c.json +15 -0
- package/workspace/test-lists/run3-d.json +6 -0
- package/workspace/test-lists/run4-a.json +8 -0
- package/workspace/test-lists/run4-b.json +6 -0
- package/workspace/test-lists/run4-c.json +13 -0
- package/workspace/test-lists/run4-d.json +9 -0
- package/workspace/test-lists/run5-a.json +6 -0
- package/workspace/test-lists/run5-b.json +6 -0
- package/workspace/test-lists/run5-c.json +15 -0
- package/workspace/test-lists/workspace-launcher.json +5 -0
- package/workspace/test-lists/workspace-phone-app.json +5 -0
- package/workspace/test-run.js +27 -1
- package/workspace/third-party/README.md +44 -0
- package/workspace/third-party/lucide-static-1.52.0.LICENSE +43 -0
- package/workspace/third-party/sortablejs-1.15.7.LICENSE +21 -0
- package/workspace/design-parity.js +0 -673
- package/workspace/public/pm/tab-task.js +0 -86
- package/workspace/public/pm/view-guide.js +0 -141
- package/workspace/public/pm/view-knowledge.js +0 -225
- package/workspace/public/pm/view-memory.js +0 -122
- package/workspace/public/pm/view-portfolio.js +0 -153
- package/workspace/public/pm/view-reports.js +0 -190
- package/workspace/public/pm/view-scheduled.js +0 -161
- package/workspace/public/pm/view-skills.js +0 -216
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/* pm/new-initiative-sheet.js — the New-initiative form (pm-os-absorb Run 2, slice 2D; PRD R17, R25).
|
|
2
|
+
*
|
|
3
|
+
* Title, Lane, Anchor, Add. An initiative with no anchor is refused here, before any request, and again by the server.
|
|
4
|
+
* The new initiative starts in Later: only the PM's press moves it into Now. The anchors on offer are the ones the List
|
|
5
|
+
* already shows (a KR or a `health:<id>`), plus one typed by hand.
|
|
6
|
+
*
|
|
7
|
+
* openNewInitiative({ repo, ws, anchors: string[], prefill?: { title, lane, from }, onAdded(id) })
|
|
8
|
+
* `prefill` is a triaged capture that still needs an anchor: the title and the lane are filled in, and `from` names the capture.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { el } from './dom.js';
|
|
12
|
+
import { button, field, sheet, toast, ensureComponentsCss } from './primitives.js';
|
|
13
|
+
import { repoLanes, repoProfile } from './data.js';
|
|
14
|
+
import { COPY } from './initiatives-copy.js';
|
|
15
|
+
import { createInitiative } from './initiatives-api.js';
|
|
16
|
+
import { changed } from './initiative-sheet.js';
|
|
17
|
+
|
|
18
|
+
const OTHER = '__other__';
|
|
19
|
+
|
|
20
|
+
/** The lanes the engine names for this repository's profile, or null when the server did not say. The server refuses a lane it does not take. */
|
|
21
|
+
export function laneNames(repo) {
|
|
22
|
+
const set = repoLanes(repo, repoProfile(repo));
|
|
23
|
+
const lanes = Array.isArray(set) ? set.map((l) => (typeof l === 'string' ? l : l && l.name)).filter(Boolean) : [];
|
|
24
|
+
return lanes.length ? lanes : null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function openNewInitiative({ repo, ws, anchors, prefill, onAdded }) {
|
|
28
|
+
ensureComponentsCss();
|
|
29
|
+
const anchorList = Array.from(new Set((anchors || []).filter(Boolean)));
|
|
30
|
+
const t = field({ label: COPY.newForm.titleLabel, kind: 'title', value: prefill && prefill.title, attrs: { 'data-in-new-title': '' } });
|
|
31
|
+
const lanes = laneNames(repo);
|
|
32
|
+
const l = lanes
|
|
33
|
+
? field({ label: COPY.newForm.laneLabel, kind: 'select', value: prefill && lanes.includes(prefill.lane) ? prefill.lane : lanes[0], options: lanes.map((x) => ({ value: x, label: x })), attrs: { 'data-in-new-lane': '' } })
|
|
34
|
+
: field({ label: COPY.newForm.laneLabel, kind: 'text', value: prefill && prefill.lane, placeholder: COPY.newForm.laneHint, attrs: { 'data-in-new-lane': '' } });
|
|
35
|
+
const opts = [{ value: '', label: COPY.newForm.anchorPick }, ...anchorList.map((a) => ({ value: a, label: a })), { value: OTHER, label: COPY.newForm.anchorOther }];
|
|
36
|
+
const a = field({ label: COPY.newForm.anchorLabel, kind: 'select', value: '', options: opts, attrs: { 'data-in-new-anchor': '' } });
|
|
37
|
+
const typed = field({ label: COPY.newForm.anchorLabel, kind: 'text', placeholder: COPY.newForm.anchorHint, attrs: { 'data-in-new-anchor-text': '' } });
|
|
38
|
+
typed.hidden = true;
|
|
39
|
+
a.control.addEventListener('change', () => { typed.hidden = a.control.value !== OTHER; if (!typed.hidden) typed.control.focus(); });
|
|
40
|
+
const err = el('p', { class: 'in-error', role: 'alert', 'data-in-error': '' });
|
|
41
|
+
err.hidden = true;
|
|
42
|
+
const lands = el('p', { class: 'in-note' }, COPY.newForm.lands);
|
|
43
|
+
const fail = (text) => { err.textContent = text; err.hidden = false; };
|
|
44
|
+
|
|
45
|
+
let busy = false;
|
|
46
|
+
const add = button({
|
|
47
|
+
label: COPY.newForm.add, variant: 'primary', attrs: { 'data-in-new-add': '' },
|
|
48
|
+
onClick: async () => {
|
|
49
|
+
if (busy) return;
|
|
50
|
+
err.hidden = true;
|
|
51
|
+
const title = t.control.value.trim();
|
|
52
|
+
const pick = a.control.value;
|
|
53
|
+
const anchor = (pick === OTHER ? typed.control.value : pick).trim();
|
|
54
|
+
if (!title) { fail(COPY.newForm.noTitle); t.control.focus(); return; }
|
|
55
|
+
if (!anchor) { fail(COPY.newForm.noAnchor); return; }
|
|
56
|
+
busy = true; add.disabled = true;
|
|
57
|
+
const out = await createInitiative(ws, { title, ...(l.control.value.trim() ? { lane: l.control.value.trim() } : {}), anchor, ...(prefill && prefill.from ? { from: prefill.from } : {}) });
|
|
58
|
+
busy = false; add.disabled = false;
|
|
59
|
+
if (out && out.ok) {
|
|
60
|
+
const id = (out.initiative && out.initiative.id) || out.id || (out.ids && out.ids[0]) || null;
|
|
61
|
+
s.close();
|
|
62
|
+
toast({ text: COPY.newForm.added(title), timeout: 4000 });
|
|
63
|
+
changed();
|
|
64
|
+
if (onAdded) onAdded(id);
|
|
65
|
+
} else fail(COPY.refused((out && out.error) || 'The server did not answer.'));
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
const s = sheet({
|
|
69
|
+
mode: 'auto', title: COPY.newForm.title, body: [t, l, a, typed, lands, err], attrs: { 'data-in-sheet': 'new' },
|
|
70
|
+
actions: [button({ label: COPY.cancel, variant: 'outline', onClick: () => s.close(), attrs: { 'data-in-cancel': '' } }), add],
|
|
71
|
+
});
|
|
72
|
+
t.control.focus();
|
|
73
|
+
return s;
|
|
74
|
+
}
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { el } from './dom.js';
|
|
16
16
|
import { state, setState, FIXTURE } from './state.js';
|
|
17
|
+
import { askText } from './dialogs.js';
|
|
17
18
|
|
|
18
19
|
let dataMod = null;
|
|
19
20
|
const dataReady = import('./data.js').then((m) => { dataMod = m; }).catch(() => { dataMod = null; });
|
|
@@ -22,7 +23,7 @@ import('./panel-initiatives.js').then((m) => { iniMod = m; }).catch(() => { iniM
|
|
|
22
23
|
|
|
23
24
|
const LAB = 'font-family:var(--mono);font-size:9.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted-foreground)';
|
|
24
25
|
const BTN = 'font-family:var(--mono);font-size:10px;padding:2px 7px;background:var(--secondary);border:1px solid var(--border);color:var(--foreground);cursor:pointer;min-height:24px';
|
|
25
|
-
const GO = 'font
|
|
26
|
+
const GO = 'font:11px var(--mono);padding:3px 10px;background:var(--primary);border:1px solid var(--primary);color:var(--primary-foreground);cursor:pointer;min-height:26px';
|
|
26
27
|
const iv = (t) => el('span', null, t == null ? '' : String(t));
|
|
27
28
|
|
|
28
29
|
/* per-item UI state, keyed by item id: { loading, plan, judgment, profile, note, summary } */
|
|
@@ -58,7 +59,7 @@ async function review(slug, it, verdict) {
|
|
|
58
59
|
const u = slot(it.id);
|
|
59
60
|
let note = '';
|
|
60
61
|
if (verdict === 'send-back') {
|
|
61
|
-
note =
|
|
62
|
+
note = await askText({ title: 'Send “' + (it.key || it.text) + '” back', label: 'What should change? One line.', confirmLabel: 'Send back' });
|
|
62
63
|
if (note === null || !String(note).trim()) return;
|
|
63
64
|
}
|
|
64
65
|
const out = await dataMod.nextStepPost('review', { session_id: it.run.session_id, verdict, note: String(note).trim() });
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/* pm/palette-copy.js — every word the ⌘K palette shows (pm-os-absorb Run 3, slice 3C).
|
|
2
|
+
*
|
|
3
|
+
* Pure data and pure functions. No DOM and no import, so `glossary.test.js` can load this file in Node and check every
|
|
4
|
+
* string against the glossary rules. Written in the PMOS-STE instructional register: short sentences, one instruction
|
|
5
|
+
* each, the same word for the same thing.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export const COPY = {
|
|
9
|
+
title: 'Search or run',
|
|
10
|
+
sub: 'Open anything, run a command, or keep a note.',
|
|
11
|
+
inputLabel: 'Search or type a command',
|
|
12
|
+
placeholder: 'Type a name, a command, or a note',
|
|
13
|
+
memoryEntry: { title: 'Memory entry', close: 'Close' },
|
|
14
|
+
scopeLabel: 'Search in',
|
|
15
|
+
scopes: { all: 'All', knowledge: 'Knowledge', memory: 'Memory', guide: 'Guide' },
|
|
16
|
+
kinds: {
|
|
17
|
+
command: 'Command', destination: 'Go to', initiative: 'Initiative', repository: 'Repository', session: 'Session',
|
|
18
|
+
tab: 'Tab', view: 'View', knowledge: 'Document', memory: 'Memory', guide: 'Guide', capture: 'Note', ask: 'Ask',
|
|
19
|
+
},
|
|
20
|
+
hint: 'Type to search. Press Enter to open the first row.',
|
|
21
|
+
hintTyped: 'Type to search. Capture as note keeps what you typed.',
|
|
22
|
+
noMatch: (q) => `Nothing matches "${q}".`,
|
|
23
|
+
loading: (scope) => `Reading ${scope}.`,
|
|
24
|
+
unreadable: (scope, why) => `The ${scope} could not be read. ${why}`,
|
|
25
|
+
noScopeMatch: (scope) => `No ${scope} entry matches.`,
|
|
26
|
+
capture: {
|
|
27
|
+
label: 'Capture as note',
|
|
28
|
+
hint: 'Keeps what you typed as a note. Nothing else changes.',
|
|
29
|
+
saved: (id) => `Saved as a note: ${id}.`,
|
|
30
|
+
failed: (why) => `The note was not saved. ${why}`,
|
|
31
|
+
needsRepo: 'Select a repository first. A note is kept in one repository.',
|
|
32
|
+
},
|
|
33
|
+
ask: {
|
|
34
|
+
label: (name) => `Ask ${name}`,
|
|
35
|
+
hint: 'Sends what you typed to this session, then presses Enter.',
|
|
36
|
+
sent: (name) => `Sent to ${name}.`,
|
|
37
|
+
failed: (name) => `${name} is not connected. Nothing was sent.`,
|
|
38
|
+
},
|
|
39
|
+
compose: {
|
|
40
|
+
label: 'Message to the session',
|
|
41
|
+
placeholder: 'Type a message for this session',
|
|
42
|
+
send: 'Send',
|
|
43
|
+
sendLabel: 'Send the message to this session',
|
|
44
|
+
sent: (name) => `Sent to ${name}.`,
|
|
45
|
+
failed: (name) => `${name} is not connected. Nothing was sent. Your message is still here.`,
|
|
46
|
+
},
|
|
47
|
+
keys: 'Up and Down move. Enter opens. Esc closes.',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** Rules that are right here and wrong elsewhere. Nothing is lifted: every glossary rule holds on this surface. */
|
|
51
|
+
export const GLOSSARY = { exempt: {}, extra: [], guide: 'command-palette.md' };
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/* pm/palette.js — the ⌘K palette (pm-os-absorb Run 3, slice 3C; PRD R11, R1 and R16).
|
|
2
|
+
*
|
|
3
|
+
* ⌘K is the one input outside the terminal. Its rows are, in this order:
|
|
4
|
+
* 1. the rows that match what is typed: commands first (each with its CLI form, R11), then destinations, then
|
|
5
|
+
* initiatives, repositories, sessions, tabs and views, then a few hits from the Knowledge, Memory and Guide scopes;
|
|
6
|
+
* 2. Capture as note, whenever something is typed. It keeps the words as a note file and decides nothing, so it is
|
|
7
|
+
* always safe (D71). When nothing else matches, it is the top row;
|
|
8
|
+
* 3. Ask <bound session>, when a session is open: the typed text goes to that session as if typed there.
|
|
9
|
+
* Knowledge, Memory and Guide are search scopes (the tabs above the field). They read the routes that already exist
|
|
10
|
+
* (/api/library, /api/memory, /api/guide), so ⌘K reaches strategy documents and decision records as files, never as rows
|
|
11
|
+
* of the Initiatives views (D86 point 4).
|
|
12
|
+
*
|
|
13
|
+
* The palette is the shared sheet (primitives.js `sheet`), driven by `state.paletteOpen`: app.js sets that key on ⌘K and
|
|
14
|
+
* on Esc, and panel-cockpit.js `renderPalette` calls `paintPalette` on every render. The field and the scope tabs are made
|
|
15
|
+
* once while it is open, so a repaint never moves the caret. The list is rebuilt only when its rows change, so a press in
|
|
16
|
+
* the middle of a poll is not lost.
|
|
17
|
+
*
|
|
18
|
+
* What this file does not own: the commands (each run registers its own, pm/registry.js `registerCommand`), the base rows
|
|
19
|
+
* (panel-cockpit.js `paletteItems`), and the capture note itself (lib/capture.js, POST /api/capture).
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { el } from './dom.js';
|
|
23
|
+
import { state, setState, FIXTURE } from './state.js';
|
|
24
|
+
import { sheet, field, tabs, row, badge, signal, toast, button, dialogText, ensureComponentsCss } from './primitives.js';
|
|
25
|
+
import { ensureSprite } from './icons.js';
|
|
26
|
+
import { pmGet, pmPost } from './http.js';
|
|
27
|
+
import { COPY } from './palette-copy.js';
|
|
28
|
+
|
|
29
|
+
/* The sheet and the rows are primitives, so their stylesheet is asked for at load, not at the first ⌘K: a palette opened before
|
|
30
|
+
* it arrives would flash unstyled. Fixture mode loads none (the parity lock). */
|
|
31
|
+
if (!FIXTURE) ensureComponentsCss();
|
|
32
|
+
|
|
33
|
+
const TTL = 15000;
|
|
34
|
+
const SCOPES = ['all', 'knowledge', 'memory', 'guide'];
|
|
35
|
+
/** The order the kinds are listed in. A kind not named here keeps the order the base rows came in, after these. */
|
|
36
|
+
const KIND_ORDER = ['command', 'destination'];
|
|
37
|
+
const CAP = { command: 8, knowledge: 5, memory: 3, guide: 3 };
|
|
38
|
+
const CAP_DEFAULT = 6;
|
|
39
|
+
const MAX_ROWS = 40;
|
|
40
|
+
const MAX_DOCS = 2000;
|
|
41
|
+
|
|
42
|
+
let ui = null; // the open palette: { sheet, input, list, note, scopes, deps, sig }
|
|
43
|
+
let q = '';
|
|
44
|
+
let scope = 'all';
|
|
45
|
+
let sel = 0;
|
|
46
|
+
let rows = []; // what the list shows now
|
|
47
|
+
let noteKey = ''; // the note on show, so a repaint does not rebuild it
|
|
48
|
+
let pinned = null; // a message that stays until the next keystroke or scope change: { text, error }
|
|
49
|
+
let busy = false; // a capture or an ask is in flight
|
|
50
|
+
const reads = new Map(); // key -> { state: 'loading' | 'ok' | 'error', data, error, at }
|
|
51
|
+
|
|
52
|
+
/* ── reads: the three scopes ─────────────────────────────────────────────────────────────────────────────────────── */
|
|
53
|
+
|
|
54
|
+
function read(key, path) {
|
|
55
|
+
if (FIXTURE) return null;
|
|
56
|
+
const hit = reads.get(key);
|
|
57
|
+
if (hit && (hit.state === 'loading' || Date.now() - hit.at < TTL)) return hit;
|
|
58
|
+
const next = { state: 'loading', data: hit ? hit.data : null, error: null, at: hit ? hit.at : 0 };
|
|
59
|
+
reads.set(key, next);
|
|
60
|
+
pmGet(path).then((data) => {
|
|
61
|
+
reads.set(key, data && data.ok === false ? { state: 'error', data: null, error: String(data.error || ''), at: Date.now() } : { state: 'ok', data, error: null, at: Date.now() });
|
|
62
|
+
if (ui) setState({});
|
|
63
|
+
});
|
|
64
|
+
return next;
|
|
65
|
+
}
|
|
66
|
+
const wsId = () => state.wsId || '';
|
|
67
|
+
const readLibrary = () => read('library:' + wsId(), '/api/library?ws=' + encodeURIComponent(wsId()));
|
|
68
|
+
const readMemory = () => read('memory:' + wsId(), '/api/memory?ws=' + encodeURIComponent(wsId()));
|
|
69
|
+
const readGuide = () => read('guide', '/api/guide');
|
|
70
|
+
|
|
71
|
+
/** Every markdown document in the library answer: the repository's folders, its governance files, its knowledge bundles and its notes. */
|
|
72
|
+
function docsOf(data) {
|
|
73
|
+
const out = []; const seen = new Set();
|
|
74
|
+
const walk = (node, depth) => {
|
|
75
|
+
if (!node || out.length >= MAX_DOCS || depth > 9) return;
|
|
76
|
+
for (const d of node.docs || []) {
|
|
77
|
+
if (!d || typeof d.path !== 'string' || seen.has(d.path) || out.length >= MAX_DOCS) continue;
|
|
78
|
+
if (d.md === false || !/\.(?:md|markdown)$/i.test(d.path || d.name || '')) continue;
|
|
79
|
+
seen.add(d.path);
|
|
80
|
+
out.push({ path: d.path, name: d.name || d.path.split('/').pop(), category: d.category || '' });
|
|
81
|
+
}
|
|
82
|
+
for (const sub of node.dirs || []) walk(sub, depth + 1);
|
|
83
|
+
};
|
|
84
|
+
for (const key of ['knowledge', 'roots', 'notes']) for (const t of (data && data[key]) || []) walk(t, 0);
|
|
85
|
+
walk({ docs: (data && data.governance) || [] }, 0);
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const words = (s) => String(s || '').toLowerCase().split(/\s+/).filter(Boolean);
|
|
90
|
+
const hay = (it) => `${it.label} ${it.hint || ''} ${it.cli || ''} ${it.kind} ${it.tag || ''}`.toLowerCase();
|
|
91
|
+
const matches = (it, w) => w.every((x) => hay(it).includes(x));
|
|
92
|
+
|
|
93
|
+
/** The category the classifier gave a document, as a row signal. Empty and 'undetermined' say nothing to the PM, so they show no signal. */
|
|
94
|
+
const tagOf = (category) => { const c = String(category || '').trim(); return !c || c.toLowerCase() === 'undetermined' ? '' : c; };
|
|
95
|
+
|
|
96
|
+
/** A memory entry opens as a sheet that shows its whole text. The Memory view is cut (R29): the entries are read here, never edited. */
|
|
97
|
+
function showMemory(e) {
|
|
98
|
+
ensureComponentsCss();
|
|
99
|
+
const h = sheet({
|
|
100
|
+
mode: 'auto', title: COPY.memoryEntry.title, sub: String(e.target || 'memory'),
|
|
101
|
+
body: [dialogText(String(e.text || ''))],
|
|
102
|
+
actions: [button({ label: COPY.memoryEntry.close, variant: 'outline', onClick: () => h.close() })],
|
|
103
|
+
attrs: { 'data-pl': 'memory-entry' },
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** The hits one scope offers for the typed words, as rows. Null while the first read is under way. */
|
|
108
|
+
function scopeRows(name, w, deps) {
|
|
109
|
+
if (name === 'knowledge') {
|
|
110
|
+
const r = readLibrary(); if (!r || !r.data) return r;
|
|
111
|
+
const out = docsOf(r.data).map((d) => ({ kind: 'knowledge', label: d.name, hint: d.path, tag: tagOf(d.category), run: () => deps.openDoc(null, d.path, 'FILE', { ws: wsId(), branch: null, linked: true }) }));
|
|
112
|
+
return { state: 'ok', rows: out.filter((it) => matches(it, w)).sort((a, b) => Number(!w.every((x) => a.label.toLowerCase().includes(x))) - Number(!w.every((x) => b.label.toLowerCase().includes(x)))) };
|
|
113
|
+
}
|
|
114
|
+
if (name === 'memory') {
|
|
115
|
+
const r = readMemory(); if (!r || !r.data) return r;
|
|
116
|
+
const out = ((r.data && r.data.entries) || []).map((e) => ({ kind: 'memory', label: String(e.text || '').replace(/\s+/g, ' ').slice(0, 90), hint: String(e.target || 'memory'), run: () => showMemory(e) }));
|
|
117
|
+
return { state: 'ok', rows: out.filter((it) => matches(it, w)) };
|
|
118
|
+
}
|
|
119
|
+
const r = readGuide(); if (!r || !r.data) return r;
|
|
120
|
+
const out = ((r.data && r.data.pages) || []).map((pg) => ({ kind: 'guide', label: String(pg.title || pg.page), hint: String(pg.page),
|
|
121
|
+
run: () => deps.openTab({ id: 'guide:' + pg.page, kind: 'doc', title: pg.title || pg.page, guide: true, readonly: true, path: pg.page, slug: null, repo: state.product || null, role: 'GUIDE' }) }));
|
|
122
|
+
return { state: 'ok', rows: out.filter((it) => matches(it, w)) };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* ── the bound session, Capture and Ask ──────────────────────────────────────────────────────────────────────────── */
|
|
126
|
+
|
|
127
|
+
/** The session ⌘K asks: the open terminal tab, else the one bound to the selected initiative, else the latest in this repository. */
|
|
128
|
+
export function boundSession() {
|
|
129
|
+
const terms = (state.tabs || []).filter((t) => t.kind === 'term');
|
|
130
|
+
return terms.find((t) => t.id === state.active)
|
|
131
|
+
|| (state.sel && terms.find((t) => t.slug === state.sel))
|
|
132
|
+
|| [...terms].reverse().find((t) => !t.repo || t.repo === state.product)
|
|
133
|
+
|| null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Show a message under the list and keep it through repaints (a poll repaints the page) until the next keystroke or scope change. */
|
|
137
|
+
function say(text, error) {
|
|
138
|
+
if (!ui) return;
|
|
139
|
+
pinned = text ? { text, error: !!error } : null;
|
|
140
|
+
noteKey = '';
|
|
141
|
+
refresh();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function doCapture() {
|
|
145
|
+
const text = q.trim();
|
|
146
|
+
if (!text || busy) return false;
|
|
147
|
+
if (!wsId()) { say(COPY.capture.needsRepo, true); return false; }
|
|
148
|
+
busy = true;
|
|
149
|
+
const r = await pmPost('/api/capture', { ws: wsId(), text });
|
|
150
|
+
busy = false;
|
|
151
|
+
if (!r || r.ok !== true) { say(COPY.capture.failed(String((r && r.error) || '')), true); return false; }
|
|
152
|
+
toast({ text: COPY.capture.saved(r.id) });
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function doAsk() {
|
|
157
|
+
const text = q.trim(); const b = boundSession();
|
|
158
|
+
if (!text || !b || busy) return false;
|
|
159
|
+
const name = String(b.title || b.id);
|
|
160
|
+
let term = null;
|
|
161
|
+
try { term = await import('./tab-term.js'); } catch { term = null; }
|
|
162
|
+
if (!term || typeof term.sendToTerm !== 'function' || !term.sendToTerm(b, text + '\n')) { say(COPY.ask.failed(name), true); return false; }
|
|
163
|
+
setState({ active: b.id });
|
|
164
|
+
toast({ text: COPY.ask.sent(name) });
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/* ── the rows ────────────────────────────────────────────────────────────────────────────────────────────────────── */
|
|
169
|
+
|
|
170
|
+
function build(deps) {
|
|
171
|
+
const w = words(q);
|
|
172
|
+
let list = [];
|
|
173
|
+
let status = null; // { kind: 'loading' | 'error' | 'none', scope?, why? } for the note
|
|
174
|
+
if (scope === 'all') {
|
|
175
|
+
const base = deps.rows().filter((it) => !w.length || matches(it, w));
|
|
176
|
+
const seen = {};
|
|
177
|
+
const capped = (w.length ? base : base.filter((it) => (seen[it.kind] = (seen[it.kind] || 0) + 1) <= (CAP[it.kind] || CAP_DEFAULT)));
|
|
178
|
+
const rank = (it) => { const i = KIND_ORDER.indexOf(it.kind); return i < 0 ? KIND_ORDER.length : i; };
|
|
179
|
+
list = capped.map((it, i) => [rank(it), i, it]).sort((a, b) => a[0] - b[0] || a[1] - b[1]).map((x) => x[2]).slice(0, MAX_ROWS);
|
|
180
|
+
if (w.length) {
|
|
181
|
+
for (const name of ['knowledge', 'memory', 'guide']) {
|
|
182
|
+
const r = scopeRows(name, w, deps);
|
|
183
|
+
if (r && r.rows) list.push(...r.rows.slice(0, CAP[name]));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
} else {
|
|
187
|
+
const r = scopeRows(scope, words(q), deps);
|
|
188
|
+
const sc = COPY.scopes[scope].toLowerCase();
|
|
189
|
+
if (!r || r.state === 'loading') status = { kind: 'loading', scope: sc };
|
|
190
|
+
else if (r.state === 'error') status = { kind: 'error', scope: sc, why: r.error };
|
|
191
|
+
else { list = r.rows.slice(0, MAX_ROWS); if (!list.length) status = { kind: 'none', scope: sc }; }
|
|
192
|
+
}
|
|
193
|
+
const found = list.length;
|
|
194
|
+
if (q.trim() && !FIXTURE) {
|
|
195
|
+
list.push({ kind: 'capture', label: COPY.capture.label, hint: COPY.capture.hint, async: doCapture });
|
|
196
|
+
const b = boundSession();
|
|
197
|
+
if (b) list.push({ kind: 'ask', label: COPY.ask.label(String(b.title || b.id)), hint: COPY.ask.hint, async: doAsk });
|
|
198
|
+
}
|
|
199
|
+
return { list, found, status };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function noteFor(found, status) {
|
|
203
|
+
if (pinned) return ['pinned:' + pinned.error + pinned.text, pinned.text, pinned.error ? 'error' : 'note'];
|
|
204
|
+
if (status && status.kind === 'loading') return ['loading', COPY.loading(status.scope), 'note'];
|
|
205
|
+
if (status && status.kind === 'error') return ['error:' + status.why, COPY.unreadable(status.scope, status.why), 'error'];
|
|
206
|
+
if (status && status.kind === 'none') return ['none:' + status.scope, COPY.noScopeMatch(status.scope), 'empty'];
|
|
207
|
+
if (q.trim() && !found) return ['nomatch:' + q, COPY.noMatch(q.trim()), 'empty'];
|
|
208
|
+
return q.trim() ? ['typed', COPY.hintTyped, 'note'] : ['hint', COPY.hint, 'note'];
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function choose(it) {
|
|
212
|
+
if (!it) return;
|
|
213
|
+
if (it.async) { it.async().then((done) => { if (done) closePalette(); }); return; }
|
|
214
|
+
closePalette();
|
|
215
|
+
try { it.run(); } catch (e) { console.warn('[palette] a row failed:', (e && e.message) || e); }
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function itemRow(it, i, id) {
|
|
219
|
+
const sigs = [];
|
|
220
|
+
if (it.cli) sigs.push(el('code', { class: 'pl-cli' }, it.cli));
|
|
221
|
+
else if (it.tag) sigs.push(signal({ text: it.tag }));
|
|
222
|
+
return row({
|
|
223
|
+
title: it.label,
|
|
224
|
+
meta: [it.hint].filter(Boolean),
|
|
225
|
+
signals: sigs,
|
|
226
|
+
lead: [badge({ text: COPY.kinds[it.kind] || it.kind, tone: 'muted' })],
|
|
227
|
+
onOpen: () => choose(it),
|
|
228
|
+
openAttrs: { 'data-palette': 'item', 'data-kind': it.kind, tabindex: '-1' },
|
|
229
|
+
attrs: { class: 'pl-item', id, role: 'option', 'aria-selected': i === sel ? 'true' : 'false' },
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function refresh() {
|
|
234
|
+
if (!ui) return;
|
|
235
|
+
const { list, found, status } = build(ui.deps);
|
|
236
|
+
rows = list;
|
|
237
|
+
if (sel >= rows.length) sel = Math.max(0, rows.length - 1);
|
|
238
|
+
const sig = rows.map((it) => `${it.kind}|${it.label}|${it.hint || ''}|${it.cli || ''}`).join('\n') + '#' + sel;
|
|
239
|
+
if (sig !== ui.sig) {
|
|
240
|
+
ui.sig = sig;
|
|
241
|
+
ui.list.replaceChildren(...rows.map((it, i) => itemRow(it, i, ui.listId + '-' + i)));
|
|
242
|
+
const cur = ui.list.children[sel];
|
|
243
|
+
ui.input.control.setAttribute('aria-activedescendant', cur ? cur.id : '');
|
|
244
|
+
if (cur && cur.scrollIntoView) cur.scrollIntoView({ block: 'nearest' });
|
|
245
|
+
}
|
|
246
|
+
const [key, text, tone] = noteFor(found, status);
|
|
247
|
+
if (key !== noteKey) {
|
|
248
|
+
noteKey = key;
|
|
249
|
+
ui.note.textContent = text;
|
|
250
|
+
ui.note.className = tone === 'error' ? 'pl-note pl-note--error' : 'pl-note';
|
|
251
|
+
ui.note.setAttribute('data-palette', tone);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/* ── open, close, keys ───────────────────────────────────────────────────────────────────────────────────────────── */
|
|
256
|
+
|
|
257
|
+
function setScope(next) {
|
|
258
|
+
if (!ui || !SCOPES.includes(next) || next === scope) return;
|
|
259
|
+
scope = next; sel = 0; noteKey = ''; pinned = null;
|
|
260
|
+
ui.scopes.replaceWith(ui.scopes = scopeTabs());
|
|
261
|
+
refresh();
|
|
262
|
+
ui.input.control.focus();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const scopeTabs = () => tabs({ variant: 'line', items: SCOPES.map((id) => ({ id, label: COPY.scopes[id] })), value: scope, onChange: setScope, ariaLabel: COPY.scopeLabel, attrs: { 'data-palette': 'scopes' } });
|
|
266
|
+
|
|
267
|
+
function onKey(e) {
|
|
268
|
+
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
|
269
|
+
e.preventDefault();
|
|
270
|
+
const n = rows.length; if (!n) return;
|
|
271
|
+
sel = Math.max(0, Math.min(n - 1, sel + (e.key === 'ArrowDown' ? 1 : -1)));
|
|
272
|
+
if (ui) { ui.sig = ''; refresh(); }
|
|
273
|
+
} else if (e.key === 'Enter' && !e.isComposing) {
|
|
274
|
+
e.preventDefault();
|
|
275
|
+
choose(rows[sel]);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function openPalette(deps) {
|
|
280
|
+
ensureComponentsCss();
|
|
281
|
+
ensureSprite();
|
|
282
|
+
q = ''; scope = 'all'; sel = 0; noteKey = ''; pinned = null; busy = false;
|
|
283
|
+
const listId = 'pl-list-' + Math.random().toString(36).slice(2, 7);
|
|
284
|
+
const input = field({ label: COPY.inputLabel, hideLabel: true, placeholder: COPY.placeholder,
|
|
285
|
+
attrs: { 'data-palette': 'input', role: 'combobox', 'aria-expanded': 'true', 'aria-controls': listId, 'aria-autocomplete': 'list', spellcheck: 'false' } });
|
|
286
|
+
const list = el('div', { class: 'pl-list', role: 'listbox', id: listId, 'aria-label': COPY.title, 'data-palette': 'list' });
|
|
287
|
+
const note = el('p', { class: 'pl-note', role: 'status', 'data-palette': 'note' });
|
|
288
|
+
const scopes = scopeTabs();
|
|
289
|
+
const dlg = sheet({ mode: 'auto', title: COPY.title, sub: COPY.sub, body: [scopes, input, list, note], attrs: { 'data-palette': 'overlay' },
|
|
290
|
+
onClose: () => { ui = null; if (state.paletteOpen) setState({ paletteOpen: false }); } });
|
|
291
|
+
ui = { sheet: dlg, input, list, note, scopes, deps, sig: null, listId };
|
|
292
|
+
input.control.addEventListener('input', () => { input.control.resetCommitted(input.control.value); q = input.control.value; sel = 0; pinned = null; if (ui) { ui.sig = null; refresh(); } });
|
|
293
|
+
input.control.addEventListener('keydown', onKey);
|
|
294
|
+
readLibrary(); readMemory(); readGuide();
|
|
295
|
+
input.control.focus();
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function closePalette() {
|
|
299
|
+
const u = ui;
|
|
300
|
+
ui = null;
|
|
301
|
+
if (u && u.sheet.open) u.sheet.close();
|
|
302
|
+
if (state.paletteOpen) setState({ paletteOpen: false });
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** Called by panel-cockpit.js `renderPalette` on every render. `deps`: { rows() → the base rows, openView, openDoc, openTab }. */
|
|
306
|
+
export function paintPalette(host, deps) {
|
|
307
|
+
if (!state.paletteOpen) { if (ui) closePalette(); return host; }
|
|
308
|
+
if (!ui) openPalette(deps);
|
|
309
|
+
ui.deps = deps;
|
|
310
|
+
refresh();
|
|
311
|
+
return host;
|
|
312
|
+
}
|