create-pmos 0.55.1 → 0.55.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/payload/KIT-VERSION +1 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/state/discoveries.md +1 -1
- package/workspace/REMOTE.md +19 -0
- package/workspace/bin/pmos.js +90 -9
- package/workspace/guide/index.md +1 -1
- package/workspace/guide/layout.md +9 -5
- package/workspace/guide/today.md +4 -0
- package/workspace/lib/day-run.js +9 -3
- package/workspace/lib/first-input.js +89 -0
- package/workspace/lib/phone-cli.js +94 -0
- package/workspace/package-lock.json +1 -1
- package/workspace/package.json +1 -1
- package/workspace/public/pm/app.js +1 -1
- package/workspace/public/pm/components.css +138 -0
- package/workspace/public/pm/data.js +8 -0
- package/workspace/public/pm/dest-today.js +6 -2
- package/workspace/public/pm/dock-sessions.js +373 -0
- package/workspace/public/pm/dock.js +21 -37
- package/workspace/public/pm/frame-copy.js +50 -0
- package/workspace/public/pm/icons.js +1 -1
- package/workspace/public/pm/panel-tabs.js +46 -11
- package/workspace/public/pm/tab-term.js +13 -1
- package/workspace/public/pm/today-copy.js +5 -1
- package/workspace/public/pm/today-day.js +34 -2
- package/workspace/public/vendor/lucide-sprite.js +8 -2
- package/workspace/server.js +74 -10
- package/workspace/test-lists/workspace-phone-command.json +5 -0
- package/workspace/third-party/README.md +1 -1
|
@@ -56,6 +56,56 @@ export const COPY = {
|
|
|
56
56
|
skillsHead: 'Start from a skill',
|
|
57
57
|
noSkills: 'No skills to start from.',
|
|
58
58
|
skillsUnread: 'The skills list did not load.',
|
|
59
|
+
sideTabs: 'Show the sessions as a list at the side',
|
|
60
|
+
topTabs: 'Show the sessions as a strip on top',
|
|
61
|
+
// 0.55.1 — the dock's session list (dock-sessions.js)
|
|
62
|
+
ds: {
|
|
63
|
+
notLinked: 'Not linked',
|
|
64
|
+
scheduled: 'Scheduled',
|
|
65
|
+
openTabs: 'Open',
|
|
66
|
+
none: 'No sessions yet. Press Session to start one.',
|
|
67
|
+
group: 'Show the sessions of',
|
|
68
|
+
ended: 'ended',
|
|
69
|
+
open: (n) => 'Open ' + n,
|
|
70
|
+
more: (n) => 'Name, initiative and type of work of ' + n,
|
|
71
|
+
end: (n) => 'End ' + n,
|
|
72
|
+
endFront: 'End the session in front, or close the tab in front',
|
|
73
|
+
endAsk: (n) => 'End “' + n + '”?',
|
|
74
|
+
endYes: 'End',
|
|
75
|
+
cancel: 'Cancel',
|
|
76
|
+
endFailed: 'The session could not be ended.',
|
|
77
|
+
endedOne: (n) => 'Ended “' + n + '”.',
|
|
78
|
+
endedTree: (n, b) => 'Ended “' + n + '”. Its working copy on ' + b + ' is still there.',
|
|
79
|
+
removeCopy: 'Remove working copy',
|
|
80
|
+
copyRemoved: (b) => 'Removed the working copy of ' + b + '. The branch is kept.',
|
|
81
|
+
copyKept: (why) => 'The working copy stays: ' + why,
|
|
82
|
+
newFor: (t) => 'New session for ' + t,
|
|
83
|
+
newTitle: 'New session',
|
|
84
|
+
newSub: 'Name it, pick where it runs, then pick how it starts.',
|
|
85
|
+
name: 'Name',
|
|
86
|
+
namePh: 'Empty: named after the skill and initiative',
|
|
87
|
+
forLabel: 'For',
|
|
88
|
+
noInitiative: 'No initiative',
|
|
89
|
+
on: 'On',
|
|
90
|
+
auto: (where) => 'Automatic: ' + where,
|
|
91
|
+
mainTree: (b) => 'main tree' + (b ? ' (' + b + ')' : ''),
|
|
92
|
+
newFolder: (b) => 'new working copy on ' + b,
|
|
93
|
+
newBranch: 'New branch…',
|
|
94
|
+
branchName: 'Branch name',
|
|
95
|
+
needBranch: 'Type a name for the new branch.',
|
|
96
|
+
startHead: 'Start',
|
|
97
|
+
blank: 'Blank session',
|
|
98
|
+
blankMeta: (cli, tier) => (cli === 'claude' ? 'Claude' : 'Shell') + (cli === 'claude' && tier !== 'auto' ? ' · ' + tier : '') + ' · from Settings',
|
|
99
|
+
skillsLoading: 'Reading the skills…',
|
|
100
|
+
refused: 'The workspace server refused, without saying why.',
|
|
101
|
+
metaTitle: 'Session',
|
|
102
|
+
type: 'Type of work',
|
|
103
|
+
noType: 'No type',
|
|
104
|
+
initiative: 'Initiative',
|
|
105
|
+
save: 'Save',
|
|
106
|
+
saveFailed: 'Nothing was saved.',
|
|
107
|
+
types: { research: 'Research', prototype: 'Prototype', prd: 'PRD', build: 'Build', review: 'Review', report: 'Report', general: 'General' },
|
|
108
|
+
},
|
|
59
109
|
},
|
|
60
110
|
detail: { close: 'Close the detail' },
|
|
61
111
|
phone: {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* pm/icons.js — the lucide icons the Breakdown uses (D86 point 6).
|
|
2
2
|
*
|
|
3
|
-
* The
|
|
3
|
+
* The 35 symbols live in one vendored sprite, /vendor/lucide-sprite.js, generated from lucide-static 1.52.0 (see
|
|
4
4
|
* workspace/third-party/README.md). `ensureSprite()` loads it once and puts it in the overlay layer; `icon(name)` returns
|
|
5
5
|
* an <svg> that points at one symbol. Icons are decoration only: every one is aria-hidden, and a control that shows only
|
|
6
6
|
* an icon carries its own aria-label (primitives.js `iconButton` refuses to build one without).
|
|
@@ -35,7 +35,8 @@ import { mountDoc, disposeDoc, docText } from './tab-doc.js';
|
|
|
35
35
|
import { mountWeb, promptWeb } from './tab-web.js';
|
|
36
36
|
import { newSessionBlock, openNewSession } from './panel-newsess.js';
|
|
37
37
|
import { boardOf, currentProduct } from './board-read.js';
|
|
38
|
-
import { field, button, toast, ensureComponentsCss } from './primitives.js';
|
|
38
|
+
import { field, button, iconButton, toast, ensureComponentsCss } from './primitives.js';
|
|
39
|
+
import { COPY } from './frame-copy.js';
|
|
39
40
|
import { COPY as PAL } from './palette-copy.js';
|
|
40
41
|
import { askChoice } from './dialogs.js';
|
|
41
42
|
import { icon, ensureSprite } from './icons.js';
|
|
@@ -1084,7 +1085,11 @@ function keepSessionsLive(shown) {
|
|
|
1084
1085
|
let dockMod = null;
|
|
1085
1086
|
const inFrame = () => !!(window.pmApp && window.pmApp.frame && window.pmApp.frame.on);
|
|
1086
1087
|
import('./dock.js').then((m) => { dockMod = m; if (inFrame()) setState({}); }).catch(() => { dockMod = null; });
|
|
1088
|
+
/* 0.55.1 — the dock lists SESSIONS grouped by initiative (dock-sessions.js), in the frame and live mode only. */
|
|
1089
|
+
let dsMod = null;
|
|
1090
|
+
if (!FIXTURE) import('./dock-sessions.js').then((m) => { dsMod = m; if (inFrame()) setState({}); }).catch(() => { dsMod = null; });
|
|
1087
1091
|
let dockParts = null;
|
|
1092
|
+
let dockOrient = null;
|
|
1088
1093
|
function frameDock() {
|
|
1089
1094
|
if (!dockMod || !inFrame()) return null;
|
|
1090
1095
|
const repo = currentProduct();
|
|
@@ -1094,17 +1099,28 @@ function frameDock() {
|
|
|
1094
1099
|
ws,
|
|
1095
1100
|
session: dockMod.sessionMenuButton({
|
|
1096
1101
|
ws,
|
|
1097
|
-
|
|
1098
|
-
withOptions: (d) => { openNewSession(selSlug(), { agent: d.cli }); setState({ newSessTier: d.tier }); },
|
|
1099
|
-
openSkill: (sk) => openTab({ id: 'skill:' + sk.id, kind: 'skill', title: sk.label || sk.skill || sk.id, skillId: sk.id, repo: state.product || null, slug: null, role: 'SKILL' }),
|
|
1102
|
+
open: () => (dsMod ? dsMod.openNewSessionSheet(currentProduct(), { initiative: null }) : newSession()),
|
|
1100
1103
|
}),
|
|
1101
1104
|
fold: dockParts ? dockParts.fold : dockMod.foldButton({ fold: () => window.pmApp.frame.foldDock() }),
|
|
1102
1105
|
focus: dockParts ? dockParts.focus : dockMod.focusButton({ toggle: () => window.pmApp.frame.toggleFocus() }),
|
|
1103
1106
|
};
|
|
1104
1107
|
}
|
|
1105
|
-
if (isMobile()) return { session: dockParts.session, fold: null, focus: null };
|
|
1108
|
+
if (isMobile()) return { session: dockParts.session, fold: null, focus: null, orient: null };
|
|
1106
1109
|
dockMod.paintFocus(dockParts.focus, !!window.pmApp.frame.dockMax);
|
|
1107
|
-
|
|
1110
|
+
// 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
|
|
1111
|
+
// one element across paints, so the keyboard focus stays on it; only its icon and name follow the mode
|
|
1112
|
+
const v = !!state.tabsVertical;
|
|
1113
|
+
if (!dockOrient) {
|
|
1114
|
+
dockOrient = iconButton({ icon: 'panel-left', ariaLabel: COPY.dock.sideTabs,
|
|
1115
|
+
onClick: () => setState({ tabsVertical: !state.tabsVertical, dsEnding: null }), attrs: { 'data-tabs': 'orient' } });
|
|
1116
|
+
}
|
|
1117
|
+
const oname = v ? 'panel-top' : 'panel-left';
|
|
1118
|
+
const cur = dockOrient.querySelector('[data-icon]');
|
|
1119
|
+
if (!cur || cur.getAttribute('data-icon') !== oname) dockOrient.replaceChildren(icon(oname));
|
|
1120
|
+
const olabel = v ? COPY.dock.topTabs : COPY.dock.sideTabs;
|
|
1121
|
+
dockOrient.setAttribute('aria-label', olabel);
|
|
1122
|
+
dockOrient.title = olabel;
|
|
1123
|
+
return { session: dockParts.session, fold: dockParts.fold, focus: dockParts.focus, orient: dockOrient };
|
|
1108
1124
|
}
|
|
1109
1125
|
|
|
1110
1126
|
/**
|
|
@@ -1277,6 +1293,8 @@ export function renderTabs(host) {
|
|
|
1277
1293
|
const oi = vertical ? 'arrow-left-right' : 'arrow-up-down';
|
|
1278
1294
|
if (!orientIcon.firstChild || orientIcon.firstChild.getAttribute('data-icon') !== oi) orientIcon.replaceChildren(icon(oi));
|
|
1279
1295
|
orientBtn.title = vertical ? 'show the tabs as a strip above the panes' : 'show the tabs as a list at the left of the panes';
|
|
1296
|
+
const ds = fr && dsMod ? currentProduct() : null; // the frame's session list (0.55.1); null = the tab list as before
|
|
1297
|
+
const notListed = (listed) => visTabs.filter((t) => !(t.kind === 'term' && t.sessionId && listed.has(t.sessionId)));
|
|
1280
1298
|
if (vertical) {
|
|
1281
1299
|
tabBar.setAttribute('style', CK_VBAR);
|
|
1282
1300
|
const V_BTN = CK_BTN.replace('margin:0 0 4px', 'margin:0') + ';justify-content:start';
|
|
@@ -1314,10 +1332,25 @@ export function renderTabs(host) {
|
|
|
1314
1332
|
setChildren(tabBar, [
|
|
1315
1333
|
...visTabs.map((t) => tabNode(t, act, splitTab)),
|
|
1316
1334
|
fr ? fr.session : newSessionBtn,
|
|
1317
|
-
FIXTURE ? null : newWebBtn,
|
|
1335
|
+
FIXTURE || fr ? null : newWebBtn,
|
|
1318
1336
|
!FIXTURE && state.webNotice ? el('span', { 'data-r5': 'notice', style: 'align-self:center;font-size:11px;color:var(--destructive);padding:0 6px' }, state.webNotice) : null,
|
|
1319
|
-
newSessionOptsBtn,
|
|
1320
|
-
canSplit ? splitBtn : null,
|
|
1337
|
+
fr ? null : newSessionOptsBtn,
|
|
1338
|
+
canSplit && (!fr || state.splitId) ? splitBtn : null,
|
|
1339
|
+
]);
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
/* 0.55.1 — in the frame the bar lists SESSIONS by initiative (dock-sessions.js); the tabs that are not sessions follow
|
|
1343
|
+
* under "Open". The bar keeps the style the branch above gave it; only its children change. Web, options and split left
|
|
1344
|
+
* the frame's dock (split stays only to undo one already made). */
|
|
1345
|
+
if (ck && ds) {
|
|
1346
|
+
const others = notListed(dsMod.listedSessionIds(ds));
|
|
1347
|
+
setChildren(tabBar, vertical ? [
|
|
1348
|
+
fr.session,
|
|
1349
|
+
...dsMod.railNodes(ds, others.map((t) => cockpitTabNode(t, act, splitTab, true))),
|
|
1350
|
+
el('div', { 'data-tabs': 'vcontrols', class: 'fr-ds-foot' }, state.splitId ? splitBtn : null, fr.orient, fr.focus, fr.fold),
|
|
1351
|
+
] : [
|
|
1352
|
+
...dsMod.stripNodes(ds, others.map((t) => cockpitTabNode(t, act, splitTab))),
|
|
1353
|
+
state.splitId ? splitBtn : null, fr.orient, fr.focus, fr.fold,
|
|
1321
1354
|
]);
|
|
1322
1355
|
}
|
|
1323
1356
|
|
|
@@ -1325,7 +1358,9 @@ export function renderTabs(host) {
|
|
|
1325
1358
|
if (tabBar.scrollTop !== keepY) tabBar.scrollTop = keepY;
|
|
1326
1359
|
// 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
|
|
1327
1360
|
// long list is seen. Only the bar's own offset is moved (not scrollIntoView, which would also scroll the panels around it).
|
|
1328
|
-
|
|
1361
|
+
// …and when the bar itself changed (the session list landed, or the orientation flipped): the tab may be off screen
|
|
1362
|
+
const frontKey = act ? act.id + '|' + (ds ? 'ds' : '') + (vertical ? 'v' : 'h') : null;
|
|
1363
|
+
if (ck && act && frontKey !== lastFrontId) {
|
|
1329
1364
|
const n = [...tabBar.querySelectorAll('[data-tab]')].find((x) => x.getAttribute('data-tab') === act.id);
|
|
1330
1365
|
if (n) {
|
|
1331
1366
|
const b = tabBar.getBoundingClientRect(), r = n.getBoundingClientRect();
|
|
@@ -1336,7 +1371,7 @@ export function renderTabs(host) {
|
|
|
1336
1371
|
else if (r.right > b.right) tabBar.scrollLeft += r.right - b.right;
|
|
1337
1372
|
}
|
|
1338
1373
|
}
|
|
1339
|
-
lastFrontId =
|
|
1374
|
+
lastFrontId = frontKey;
|
|
1340
1375
|
|
|
1341
1376
|
// ── panes ──
|
|
1342
1377
|
reapClosedPanes();
|
|
@@ -241,8 +241,20 @@ export function requestSpawn(opts) {
|
|
|
241
241
|
return new Promise((resolve) => {
|
|
242
242
|
const req = {
|
|
243
243
|
kind: 'ask', resolve, notice: null, settled: false, timer: null, sent: false,
|
|
244
|
-
|
|
244
|
+
/* `start: true` (0.55.1, the dock's new-session sheet) sends the one-click `start` message instead: the server's
|
|
245
|
+
* start policy then picks the tree — the initiative's own branch, made the first time, or the main tree when
|
|
246
|
+
* there is no initiative — and nothing about branches is decided here. */
|
|
247
|
+
msg: o.start ? {
|
|
248
|
+
t: 'start', name: o.name, agent: o.agent || 'shell', ws: o.ws,
|
|
249
|
+
...(o.initiative ? { initiative: o.initiative } : {}),
|
|
250
|
+
profile: o.profile === undefined ? null : o.profile,
|
|
251
|
+
...(o.skill ? { skill: o.skill } : {}),
|
|
252
|
+
...(o.workType ? { workType: o.workType } : {}),
|
|
253
|
+
...(o.tier ? { tier: o.tier } : {}),
|
|
254
|
+
} : {
|
|
245
255
|
t: 'new',
|
|
256
|
+
...(o.skill ? { skill: o.skill } : {}),
|
|
257
|
+
...(o.workType ? { workType: o.workType } : {}),
|
|
246
258
|
name: o.name,
|
|
247
259
|
agent: o.agent || 'shell',
|
|
248
260
|
ws: o.ws,
|
|
@@ -128,7 +128,11 @@ export const COPY = {
|
|
|
128
128
|
unreached: 'The workspace did not answer.',
|
|
129
129
|
refused: (why) => `Nothing changed. ${why}`,
|
|
130
130
|
oldServer: 'This workspace server has no day run yet. Only your goals show.',
|
|
131
|
-
panels: { calendar: 'Calendar', jobs: 'Jobs', goals: 'Goals for today', mentions: 'Mentions since yesterday', detail: 'Detail' },
|
|
131
|
+
panels: { summary: 'Agent results', calendar: 'Calendar', jobs: 'Jobs', goals: 'Goals for today', mentions: 'Mentions since yesterday', waiting: 'Also waiting for you', detail: 'Detail' },
|
|
132
|
+
summary: (c) => [c.review ? `${c.review} ${c.review === 1 ? 'draft waits' : 'drafts wait'} for you` : null, c.done ? `${c.done} done` : null,
|
|
133
|
+
c.working ? `${c.working} working` : null, c.failed ? `${c.failed} failed` : null].filter(Boolean).join(' · ') || 'No agent job yet.',
|
|
134
|
+
summaryAll: (n) => `All ${n} agent ${n === 1 ? 'job has' : 'jobs have'} a result.`,
|
|
135
|
+
waitingHint: 'Outcome checks, reports and sessions from your repositories, outside today\'s jobs.',
|
|
132
136
|
now: (at) => `now ${at}`,
|
|
133
137
|
noCalendar: 'No events read for today.',
|
|
134
138
|
noJobs: 'No jobs yet. Press Prepare my day. Each job shows here; select one to see what it needs.',
|
|
@@ -378,6 +378,38 @@ function jobsPanel(ctx) {
|
|
|
378
378
|
el('ul', { class: 'td-day__rows' }, ...l.map((j) => jobRow(ctx, j))))));
|
|
379
379
|
}
|
|
380
380
|
|
|
381
|
+
/* The coordinator's view of the run: what every agent job came back with, in one place. The server runs the jobs as one
|
|
382
|
+
* session each and keeps their state; this reads that state, so it needs no session of its own. */
|
|
383
|
+
function summaryPanel(ctx) {
|
|
384
|
+
const { m } = ctx;
|
|
385
|
+
const ts = m.tasks;
|
|
386
|
+
if (!ts.length) return null;
|
|
387
|
+
const c = { review: 0, done: 0, working: 0, failed: 0 };
|
|
388
|
+
for (const t of ts) {
|
|
389
|
+
if (t.status === 'review') c.review++;
|
|
390
|
+
else if (t.status === 'done' || t.status === 'sent') c.done++;
|
|
391
|
+
else if (t.status === 'failed') c.failed++;
|
|
392
|
+
else c.working++;
|
|
393
|
+
}
|
|
394
|
+
const finished = ts.filter((t) => t.status !== 'waiting' && t.status !== 'working' && t.status !== 'sending');
|
|
395
|
+
const byTask = new Map(m.jobs.filter((j) => j.task).map((j) => [j.task.id, j]));
|
|
396
|
+
const rows = finished.map((t) => {
|
|
397
|
+
const j = byTask.get(t.id);
|
|
398
|
+
const what = t.status === 'failed' ? t.error : t.sent && t.sent.link ? t.sent.link : t.draft ? t.draft.summary : '';
|
|
399
|
+
return j ? rowBtn(ctx, 'job', j.id, { lead: '·', title: j.title, meta: what || '', trail: statusPill(j) }) : null;
|
|
400
|
+
}).filter(Boolean);
|
|
401
|
+
return panel('summary', null,
|
|
402
|
+
el('p', { class: 'td-day__note', 'data-day': 'summary' }, c.working ? D.summary(c) : D.summaryAll(ts.length) + ' ' + D.summary(c)),
|
|
403
|
+
rows.length ? el('ul', { class: 'td-day__rows', 'data-day-list': 'summary' }, ...rows) : null);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/* What waits for the PM outside today's jobs (the Today cards: outcome checks, reports, sessions). Today hands its list
|
|
407
|
+
* in, so the cards keep their own controls and the open day shows one layout. */
|
|
408
|
+
function waitingPanel(owed) {
|
|
409
|
+
if (!owed || !owed.list) return null;
|
|
410
|
+
return panel('waiting', owed.count ? String(owed.count) : null, note(D.waitingHint), owed.list, owed.more || null);
|
|
411
|
+
}
|
|
412
|
+
|
|
381
413
|
function goalsPanel(ctx) {
|
|
382
414
|
const { m } = ctx;
|
|
383
415
|
return panel('goals', D.goalsCount(m.goals.filter((g) => g.status === 'open').length, m.maxGoals), goalRows(ctx), goalAdd(ctx));
|
|
@@ -721,7 +753,7 @@ function fold(open) {
|
|
|
721
753
|
}
|
|
722
754
|
|
|
723
755
|
/** The section. `ws` is the repository the day run belongs to; `openIni(id)` opens an initiative of it. */
|
|
724
|
-
export function daySection({ ws, setState, openIni } = {}) {
|
|
756
|
+
export function daySection({ ws, setState, openIni, owed } = {}) {
|
|
725
757
|
if (FIXTURE) return null;
|
|
726
758
|
// like Today's own feeds, a read that lands re-renders only while the section is on the page: a hidden Today must not
|
|
727
759
|
// cause a whole-page render under whatever the PM is doing elsewhere
|
|
@@ -755,7 +787,7 @@ export function daySection({ ws, setState, openIni } = {}) {
|
|
|
755
787
|
c.error ? el('p', { class: 'td-day__run td-day__run--failed', role: 'status', 'data-day': 'error' }, D.refused(c.error)) : null,
|
|
756
788
|
m.d && m.d.legacy ? note(D.oldServer) : null,
|
|
757
789
|
el('div', { class: 'td-day__cols' },
|
|
758
|
-
el('div', { class: 'td-day__left' }, calendar(ctx), jobsPanel(ctx), goalsPanel(ctx), mentionsPanel(ctx)),
|
|
790
|
+
el('div', { class: 'td-day__left' }, summaryPanel(ctx), calendar(ctx), jobsPanel(ctx), goalsPanel(ctx), mentionsPanel(ctx), waitingPanel(owed)),
|
|
759
791
|
detail(ctx)));
|
|
760
792
|
}
|
|
761
793
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* lucide-sprite.js — GENERATED by workspace/third-party/build-lucide-sprite.js from lucide-static 1.52.0.
|
|
2
|
-
* Do not edit by hand. The icons: grip-vertical, plus, check, square, square-check-big, x, undo-2, flag, bot, chevron-right, chevron-down, ellipsis, circle-alert, history, arrow-up, arrow-down, diamond, settings, search, refresh-cw, chevron-left, sun, moon, arrow-left, arrow-right, globe, columns-2, arrow-up-down, arrow-left-right.
|
|
2
|
+
* Do not edit by hand. The icons: grip-vertical, plus, check, square, square-check-big, x, undo-2, flag, bot, chevron-right, chevron-down, ellipsis, circle-alert, history, arrow-up, arrow-down, diamond, settings, search, refresh-cw, chevron-left, sun, moon, arrow-left, arrow-right, globe, columns-2, arrow-up-down, arrow-left-right, panel-top, panel-left, maximize-2, minimize-2, git-branch, clock.
|
|
3
3
|
*
|
|
4
4
|
* ISC License
|
|
5
5
|
*
|
|
@@ -75,5 +75,11 @@ export const SPRITE = '<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true
|
|
|
75
75
|
+ '<symbol id="lucide-columns-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" /><path d="M12 3v18" /></symbol>'
|
|
76
76
|
+ '<symbol id="lucide-arrow-up-down" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21 16-4 4-4-4" /><path d="M17 20V4" /><path d="m3 8 4-4 4 4" /><path d="M7 4v16" /></symbol>'
|
|
77
77
|
+ '<symbol id="lucide-arrow-left-right" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3 4 7l4 4" /><path d="M4 7h16" /><path d="m16 21 4-4-4-4" /><path d="M20 17H4" /></symbol>'
|
|
78
|
+
+ '<symbol id="lucide-panel-top" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" /><path d="M3 9h18" /></symbol>'
|
|
79
|
+
+ '<symbol id="lucide-panel-left" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" /><path d="M9 3v18" /></symbol>'
|
|
80
|
+
+ '<symbol id="lucide-maximize-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6" /><path d="m21 3-7 7" /><path d="m3 21 7-7" /><path d="M9 21H3v-6" /></symbol>'
|
|
81
|
+
+ '<symbol id="lucide-minimize-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m14 10 7-7" /><path d="M20 10h-6V4" /><path d="m3 21 7-7" /><path d="M4 14h6v6" /></symbol>'
|
|
82
|
+
+ '<symbol id="lucide-git-branch" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 6a9 9 0 0 0-9 9V3" /><circle cx="18" cy="6" r="3" /><circle cx="6" cy="18" r="3" /></symbol>'
|
|
83
|
+
+ '<symbol id="lucide-clock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /><path d="M12 6v6l4 2" /></symbol>'
|
|
78
84
|
+ '</svg>';
|
|
79
|
-
export const ICONS = ["grip-vertical","plus","check","square","square-check-big","x","undo-2","flag","bot","chevron-right","chevron-down","ellipsis","circle-alert","history","arrow-up","arrow-down","diamond","settings","search","refresh-cw","chevron-left","sun","moon","arrow-left","arrow-right","globe","columns-2","arrow-up-down","arrow-left-right"];
|
|
85
|
+
export const ICONS = ["grip-vertical","plus","check","square","square-check-big","x","undo-2","flag","bot","chevron-right","chevron-down","ellipsis","circle-alert","history","arrow-up","arrow-down","diamond","settings","search","refresh-cw","chevron-left","sun","moon","arrow-left","arrow-right","globe","columns-2","arrow-up-down","arrow-left-right","panel-top","panel-left","maximize-2","minimize-2","git-branch","clock"];
|
package/workspace/server.js
CHANGED
|
@@ -85,6 +85,7 @@ const repoWatch = require('./lib/repo-watch.js');
|
|
|
85
85
|
tsSettings.adoptEnv(process.env); // D79 — take the env key into memory and delete it, before anything is spawned
|
|
86
86
|
const scheduleLogic = require('./lib/schedule-logic');
|
|
87
87
|
const { safeFirstInput } = require('./lib/prompt-input');
|
|
88
|
+
const firstInput = require('./lib/first-input');
|
|
88
89
|
const transcriptsLib = require('./lib/transcripts');
|
|
89
90
|
const initiativeFiles = require('./lib/initiative-files');
|
|
90
91
|
const { deriveNextSteps } = require('./lib/next-steps');
|
|
@@ -2391,6 +2392,7 @@ function persist(s) {
|
|
|
2391
2392
|
meta.initiative = s.initiative == null ? null : s.initiative;
|
|
2392
2393
|
meta.attributedBy = s.attributedBy || null;
|
|
2393
2394
|
}
|
|
2395
|
+
if (s.workType) meta.workType = s.workType;
|
|
2394
2396
|
try {
|
|
2395
2397
|
fs.writeFileSync(path.join(SESS_DIR, s.id + '.json'), JSON.stringify(meta));
|
|
2396
2398
|
fs.writeFileSync(path.join(SESS_DIR, s.id + '.scrollback'), s.buf);
|
|
@@ -2703,6 +2705,7 @@ function sessionList() {
|
|
|
2703
2705
|
// make an inference indistinguishable from a record — the exact confusion this field removes.
|
|
2704
2706
|
initiative: 'initiative' in s ? (s.initiative == null ? null : s.initiative) : null,
|
|
2705
2707
|
attributed_by: 'initiative' in s ? (s.attributedBy || null) : null,
|
|
2708
|
+
work_type: s.workType || null,
|
|
2706
2709
|
profile_id: 'profile' in s ? (s.profile == null ? null : s.profile) : null,
|
|
2707
2710
|
profile_line: profilesLib.profileLine(
|
|
2708
2711
|
!('profile' in s) ? undefined
|
|
@@ -3095,7 +3098,27 @@ function resolveProfileForSpawn(id) {
|
|
|
3095
3098
|
: `profile "${id}" is not among the descriptors in ${read.dir} — PMOS will not substitute another` };
|
|
3096
3099
|
}
|
|
3097
3100
|
|
|
3098
|
-
|
|
3101
|
+
/* The type of work a session is doing (0.55.1): the dock shows it under the session's name. Set by what started the
|
|
3102
|
+
* session (a skill, a schedule) or chosen by the PM; changed later from the session's ⋯ sheet. Optional: a session
|
|
3103
|
+
* with none shows nothing, and a session started before this field existed restores with none. */
|
|
3104
|
+
const WORK_TYPES = Object.freeze(['research', 'prototype', 'prd', 'build', 'review', 'report', 'general']);
|
|
3105
|
+
const workTypeOrNull = (v) => (WORK_TYPES.includes(String(v || '').toLowerCase()) ? String(v).toLowerCase() : null);
|
|
3106
|
+
|
|
3107
|
+
/* Starting a session FROM a skill (0.55.1): the same launch a scheduled run makes (fireSchedule), on the PM's press —
|
|
3108
|
+
* the skill's agent, its prompt (filing contract retargeted for this workspace) as the first input, and its tier. */
|
|
3109
|
+
function skillLaunch(wsIdArg, skillId) {
|
|
3110
|
+
const w = getWs(wsIdArg) || homeWorkspace();
|
|
3111
|
+
const root = w ? w.root : SELF_ROOT;
|
|
3112
|
+
const tasks = loadTasks(root);
|
|
3113
|
+
const id = String(skillId || '');
|
|
3114
|
+
const task = tasks.find((t) => t.id === id) || tasks.find((t) => t.skill === id) || null;
|
|
3115
|
+
if (!task) return { ok: false, error: `no skill named ${id.slice(0, 60)} in this repository` };
|
|
3116
|
+
const prompt = task.prompt ? skillStore.retargetFiling(task.prompt, filingContract(w)) : null;
|
|
3117
|
+
return { ok: true, agent: task.agent || 'claude', initialInput: prompt ? prompt + '\r' : null,
|
|
3118
|
+
modelAsk: { skillTier: task.tier || null, skill: task.skill } };
|
|
3119
|
+
}
|
|
3120
|
+
|
|
3121
|
+
function spawnSession({ name, agent, cwd, ws, origin, initialInput, profile, initiative, modelAsk, planItem, workType }) {
|
|
3099
3122
|
const id = nextSessionId();
|
|
3100
3123
|
/* pm-os-absorb Run 1 — every AGENT session gets a token, `<id>.<8 hex>`, in its environment as PMOS_SESSION, and
|
|
3101
3124
|
* PMOS_BIN, this workspace's own bin/pmos.js. `pmos item` run inside it is then attributed `agent:<token>` (the token
|
|
@@ -3113,7 +3136,9 @@ function spawnSession({ name, agent, cwd, ws, origin, initialInput, profile, ini
|
|
|
3113
3136
|
// a scheduled run tags the session with its origin, and feeds the skill invocation as first input
|
|
3114
3137
|
const tag = origin || null;
|
|
3115
3138
|
// safeFirstInput: a first input that begins with `/` is read by the agent as a slash command, not as the task.
|
|
3116
|
-
|
|
3139
|
+
// firstInput.deliver waits until the agent is ready to read, types the prompt, then presses Enter as its own key
|
|
3140
|
+
// (lib/first-input.js): a burst written at a fixed 700 ms was lost or left unsent while the agent was still starting.
|
|
3141
|
+
const feed = (s) => { if (initialInput && s && s.pty) { try { firstInput.deliver(s, safeFirstInput(initialInput)); } catch {} } };
|
|
3117
3142
|
// shell gets a real fallback to `sh`; agent CLIs run as-is (harness untouched), NO fallback —
|
|
3118
3143
|
// falling back from `claude` to `sh` would be a lie.
|
|
3119
3144
|
const cmd = isShell ? (commandExists(SHELL) ? SHELL : 'sh') : agent;
|
|
@@ -3141,6 +3166,8 @@ function spawnSession({ name, agent, cwd, ws, origin, initialInput, profile, ini
|
|
|
3141
3166
|
// Hand to agent: the breakdown item this session was started for, or null. Set only by /api/plan-items/handoff/go.
|
|
3142
3167
|
base.plan_item = planItem || null;
|
|
3143
3168
|
base.token = sessionToken;
|
|
3169
|
+
const wt = workTypeOrNull(workType);
|
|
3170
|
+
if (wt) base.workType = wt;
|
|
3144
3171
|
|
|
3145
3172
|
/* WHICH MODEL (D75). Resolved once, here, with the profile, so the record can say only what was applied.
|
|
3146
3173
|
* Three states are kept apart — not asked, applied, and "this binary has no model control" — and a choice that
|
|
@@ -3259,7 +3286,7 @@ function spawnSession({ name, agent, cwd, ws, origin, initialInput, profile, ini
|
|
|
3259
3286
|
// A FAILED git operation in a real repo must never spawn a session on the wrong branch (rubric
|
|
3260
3287
|
// hard floor): we return {ok:false} and spawn nothing. Kept as its own function so the
|
|
3261
3288
|
// git-then-maybe-spawn contract is unit-testable without a WS client.
|
|
3262
|
-
function spawnWithBranch({ name, agent, cwd, ws, branch, createBranch, isolate, profile, initiative, modelAsk, origin, initialInput, planItem }) {
|
|
3289
|
+
function spawnWithBranch({ name, agent, cwd, ws, branch, createBranch, isolate, profile, initiative, modelAsk, origin, initialInput, planItem, workType }) {
|
|
3263
3290
|
const w = getWs(ws);
|
|
3264
3291
|
if (!w) return { ok: false, error: 'no workspace' };
|
|
3265
3292
|
// The profile is checked BEFORE any git operation. A composer that picked a profile PMOS cannot
|
|
@@ -3306,7 +3333,7 @@ function spawnWithBranch({ name, agent, cwd, ws, branch, createBranch, isolate,
|
|
|
3306
3333
|
broadcast({ t: 'branch', ws: w.id, ...gitInfo.branchInfo(w.root) }); // let every client refresh the chip
|
|
3307
3334
|
}
|
|
3308
3335
|
}
|
|
3309
|
-
const s = spawnSession({ name, agent: agent || 'shell', cwd, ws: targetWs, profile: pres.id, initiative, modelAsk, origin, initialInput, planItem });
|
|
3336
|
+
const s = spawnSession({ name, agent: agent || 'shell', cwd, ws: targetWs, profile: pres.id, initiative, modelAsk, origin, initialInput, planItem, workType });
|
|
3310
3337
|
// The co-tenancy fact, on the ordinary path too. Note where this sits: OUTSIDE the `if (branch)`
|
|
3311
3338
|
// block above. The isolate default already computes that other sessions would be disturbed — and
|
|
3312
3339
|
// then the whole git block is skipped when no branch was picked, which is the common path and the
|
|
@@ -3409,7 +3436,7 @@ function startPreview({ initiative, repoKey, ws }) {
|
|
|
3409
3436
|
// One click. Resolves the policy, then hands the SAME seam spawnWithBranch already exposes the
|
|
3410
3437
|
// branch/isolate decision it made — so the inherited hard floor (a failed git operation spawns
|
|
3411
3438
|
// nothing) is enforced in exactly one place and is not re-implemented here.
|
|
3412
|
-
function startWork({ initiative, repoKey, ws, agent, name, profile, origin, initialInput, planItem }) {
|
|
3439
|
+
function startWork({ initiative, repoKey, ws, agent, name, profile, origin, initialInput, planItem, workType, modelAsk }) {
|
|
3413
3440
|
const key = repoKeyFor({ repoKey, ws });
|
|
3414
3441
|
if (!key) return { ok: false, error: 'no repository selected' };
|
|
3415
3442
|
const trees = treesOfRepo(key);
|
|
@@ -3437,6 +3464,8 @@ function startWork({ initiative, repoKey, ws, agent, name, profile, origin, init
|
|
|
3437
3464
|
origin, initialInput,
|
|
3438
3465
|
// Hand to agent (pm-os-absorb Run 1): only /api/plan-items/handoff/go passes it.
|
|
3439
3466
|
planItem,
|
|
3467
|
+
// 0.55.1 — the dock's new-session sheet: the type of work, and a skill's model ask.
|
|
3468
|
+
workType, modelAsk: modelAsk || null,
|
|
3440
3469
|
});
|
|
3441
3470
|
if (!r.ok) return r;
|
|
3442
3471
|
return { ok: true, session: r.session, notice: r.notice || plan.why, plan: plan.plan };
|
|
@@ -3621,8 +3650,15 @@ function daySkillPrompt(run, skillId) {
|
|
|
3621
3650
|
function startDayTask(run, t, now) {
|
|
3622
3651
|
const sk = daySkillPrompt(run, 'day-task');
|
|
3623
3652
|
let s = null;
|
|
3624
|
-
|
|
3625
|
-
|
|
3653
|
+
const line = `This run: /day-task ${t.id} ${t.mode} — read the task first with: pmos proactive day show ${t.id}`;
|
|
3654
|
+
// The session that did the task's last step is used again while it is open: it has the skill and the task in its
|
|
3655
|
+
// context already, so only this run's line is typed into it, and the PM keeps one session per job.
|
|
3656
|
+
const prev = t.prev_session && sessions.get(t.prev_session);
|
|
3657
|
+
if (!sk.error && prev && prev.state !== 'exited' && prev.pty) {
|
|
3658
|
+
try { firstInput.deliver(prev, line + '\r'); s = prev; } catch { s = null; }
|
|
3659
|
+
}
|
|
3660
|
+
if (!sk.error && !s) {
|
|
3661
|
+
const prompt = `${sk.prompt}\n\n${line}`;
|
|
3626
3662
|
s = spawnSession({ name: `${t.title} · day task`, agent: sk.task.agent || 'claude', ws: sk.w ? sk.w.id : run.ws, origin: 'day-task', initialInput: prompt + '\r',
|
|
3627
3663
|
modelAsk: { scheduleTier: null, skillTier: sk.task.tier || null, skill: sk.task.skill } });
|
|
3628
3664
|
}
|
|
@@ -6280,6 +6316,25 @@ async function handleRequest(req, res) {
|
|
|
6280
6316
|
boardCache.invalidate(root);
|
|
6281
6317
|
return json(res, { ok: true, id, lane, previous: before, changed: true });
|
|
6282
6318
|
}
|
|
6319
|
+
if (p === '/api/session/meta' && req.method === 'POST') {
|
|
6320
|
+
// 0.55.1 — the dock's ⋯ sheet: a session's NAME and TYPE OF WORK. Labels only: no checkout, no respawn. The
|
|
6321
|
+
// initiative has its own route (below) because it carries provenance; these two are just what the PM calls it.
|
|
6322
|
+
let body = {};
|
|
6323
|
+
try { body = JSON.parse((await readBody(req)) || '{}'); }
|
|
6324
|
+
catch { return json(res, { ok: false, error: 'invalid JSON body' }, 400); }
|
|
6325
|
+
const g = pmGuard.requirePmPress(req, body, { act: 'bind' });
|
|
6326
|
+
if (!g.ok) return json(res, { ok: false, error: g.error }, g.status);
|
|
6327
|
+
const s = sessions.get(String(body.id || ''));
|
|
6328
|
+
if (!s) return json(res, { ok: false, error: 'no such session' }, 404);
|
|
6329
|
+
if ('workType' in body) {
|
|
6330
|
+
const wt = body.workType === null || body.workType === '' ? null : workTypeOrNull(body.workType);
|
|
6331
|
+
if (body.workType && !wt) return json(res, { ok: false, error: 'type of work must be one of ' + WORK_TYPES.join(', ') }, 400);
|
|
6332
|
+
if (wt) s.workType = wt; else delete s.workType;
|
|
6333
|
+
}
|
|
6334
|
+
if (typeof body.name === 'string' && body.name.trim()) renameSession(s.id, body.name);
|
|
6335
|
+
persist(s); pushSessions();
|
|
6336
|
+
return json(res, { ok: true, id: s.id, name: s.name, work_type: s.workType || null });
|
|
6337
|
+
}
|
|
6283
6338
|
if (p === '/api/session/initiative' && req.method === 'POST') {
|
|
6284
6339
|
/* Bind a RUNNING session to an initiative — the PM's own word about work already in flight
|
|
6285
6340
|
* (workspace-session-attribution R3/R8).
|
|
@@ -7902,9 +7957,13 @@ wss.on('connection', (ws) => {
|
|
|
7902
7957
|
// `initiative` is what the client has always held and never sent: `+ session` is clicked on
|
|
7903
7958
|
// an initiative row, the tab carries its `slug`, and the spawn message named three other
|
|
7904
7959
|
// fields. An absent or unusable value leaves the session unattributed — never a refused spawn.
|
|
7905
|
-
|
|
7960
|
+
// 0.55.1 — a session started FROM a skill: the skill's agent, prompt and tier, resolved here and never sent.
|
|
7961
|
+
const sk = m.skill ? skillLaunch(m.ws, m.skill) : null;
|
|
7962
|
+
if (sk && !sk.ok) { ws.send(JSON.stringify({ t: 'error', error: 'skill: ' + sk.error })); return; }
|
|
7963
|
+
const r = spawnWithBranch({ name: m.name, agent: sk ? sk.agent : m.agent, cwd: m.cwd, ws: m.ws, branch: m.branch, createBranch: m.createBranch, isolate: m.isolate, profile: m.profile, initiative: wsBindable(ws, m.initiative),
|
|
7906
7964
|
// D75 — what the composer chose (a tier, or a model), carried like the profile. Absent = not asked.
|
|
7907
|
-
modelAsk: (m.tier || m.model) ? { explicit: { tier: m.tier, model: m.model, effort: m.effort } } : null
|
|
7965
|
+
modelAsk: sk ? sk.modelAsk : ((m.tier || m.model) ? { explicit: { tier: m.tier, model: m.model, effort: m.effort } } : null),
|
|
7966
|
+
initialInput: sk ? sk.initialInput : null, workType: m.workType });
|
|
7908
7967
|
// A refused profile is its own sentence, not "branch: …" — the composer's error line names
|
|
7909
7968
|
// what PMOS could not read, and nothing was started.
|
|
7910
7969
|
if (!r.ok) { ws.send(JSON.stringify({ t: 'error', error: (/^(profile|model choice) /.test(r.error || '') ? '' : 'branch: ') + r.error })); return; }
|
|
@@ -7916,8 +7975,13 @@ wss.on('connection', (ws) => {
|
|
|
7916
7975
|
// repo-first R1 — the one-click path. It carries an INITIATIVE (or none) and a repository,
|
|
7917
7976
|
// never a branch and never an isolate flag: those are the policy's to decide, and a client
|
|
7918
7977
|
// that could still send them would be a second place the rule lives.
|
|
7978
|
+
const sk = m.skill ? skillLaunch(m.ws, m.skill) : null;
|
|
7979
|
+
if (sk && !sk.ok) { ws.send(JSON.stringify({ t: 'error', error: 'skill: ' + sk.error })); return; }
|
|
7919
7980
|
const r = startWork({ initiative: wsBindable(ws, m.initiative), repoKey: m.repoKey, ws: m.ws,
|
|
7920
|
-
agent: m.agent, name: m.name, profile: m.profile
|
|
7981
|
+
agent: sk ? sk.agent : m.agent, name: m.name, profile: m.profile,
|
|
7982
|
+
initialInput: sk ? sk.initialInput : null, workType: m.workType,
|
|
7983
|
+
// a blank session from the dock carries the Settings tier (D75); a skill's own tier wins
|
|
7984
|
+
modelAsk: sk ? sk.modelAsk : (m.tier ? { explicit: { tier: m.tier } } : null) });
|
|
7921
7985
|
if (!r.ok) { ws.send(JSON.stringify({ t: 'error', error: (/^profile /.test(r.error || '') ? '' : 'start: ') + r.error })); return; }
|
|
7922
7986
|
if (r.notice) ws.send(JSON.stringify({ t: 'notice', notice: r.notice }));
|
|
7923
7987
|
ws.send(JSON.stringify({ t: 'created', id: r.session.id }));
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "workspace-phone-command: `pmos phone` — lib/phone-cli.js reading the startup banner back, and the real launcher's reuse, restart and Tailscale-refused paths on a scratch port.",
|
|
3
|
+
"modules": ["phone-command.test.js"],
|
|
4
|
+
"seconds": { "phone-command.test.js": 4 }
|
|
5
|
+
}
|
|
@@ -12,7 +12,7 @@ The workspace runs with no network (D62). Two third-party browser files are comm
|
|
|
12
12
|
| Package | Version | Licence | Tarball | Tarball sha256 | Shipped file |
|
|
13
13
|
|---|---|---|---|---|---|
|
|
14
14
|
| sortablejs | 1.15.7 | MIT | https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.7.tgz | `66cbfc8fa18860783584fe0c346baf6cbf3399da72d23d21053425ba232abed4` | `public/vendor/Sortable.min.js`: an unmodified copy of `package/Sortable.min.js` (45 478 bytes, sha256 `bf4241bc73fef7f11c59a283a69fe8051cdd31c6d8ff5a2b9ba219e7831fcf76`; npm integrity `sha512-Kk8wLQPlS+yi1ZEf48a4+fzHa4yxjC30M/Sr2AnQu+f/MPwvvX9XjZ6OWejiz8crBsLwSq8GHqaxaET7u6ux0A==`) |
|
|
15
|
-
| lucide-static | 1.52.0 | ISC, plus the Feather MIT notice | https://registry.npmjs.org/lucide-static/-/lucide-static-1.52.0.tgz | `4d2aa079171e4eba4c704844b8b0c9edb6e4cfb2fb8bd86f7b14353994f04a87` | `public/vendor/lucide-sprite.js`: generated by `build-lucide-sprite.js` (
|
|
15
|
+
| lucide-static | 1.52.0 | ISC, plus the Feather MIT notice | https://registry.npmjs.org/lucide-static/-/lucide-static-1.52.0.tgz | `4d2aa079171e4eba4c704844b8b0c9edb6e4cfb2fb8bd86f7b14353994f04a87` | `public/vendor/lucide-sprite.js`: generated by `build-lucide-sprite.js` (13 525 bytes, sha256 `3bb7ec3369206ff1bc8c6ae5dfd91c5d99e5ec89c4357d18b744b594ea366447`). It holds 35 icons: grip-vertical, plus, check, square, square-check-big, x, undo-2, flag, bot, chevron-right, chevron-down, ellipsis, circle-alert, history, arrow-up, arrow-down, diamond, then the eight the frame's chrome draws (Run 3): settings, search, refresh-cw, chevron-left, sun, moon, arrow-left, arrow-right, then globe, columns-2, arrow-up-down and arrow-left-right for the dock strip's web, split and orientation chips (Run 3, C6), then panel-top, panel-left, maximize-2, minimize-2, git-branch and clock for the dock's icon controls and session rows (0.54.2) |
|
|
16
16
|
|
|
17
17
|
The licences are in this folder: [sortablejs-1.15.7.LICENSE](sortablejs-1.15.7.LICENSE) and [lucide-static-1.52.0.LICENSE](lucide-static-1.52.0.LICENSE). The sprite also carries the lucide licence text as its first comment.
|
|
18
18
|
|