create-pmos 0.96.0 → 0.97.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 +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/guide/index.md +1 -1
- package/workspace/guide/layout.md +3 -3
- package/workspace/lib/claude-flags.js +104 -0
- package/workspace/lib/claude-flags.json +992 -0
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/components.css +22 -0
- package/workspace/public/pm/data.js +6 -0
- package/workspace/public/pm/dest-settings.js +101 -4
- package/workspace/public/pm/dock-sessions.js +4 -1
- package/workspace/public/pm/dock.js +9 -3
- package/workspace/public/pm/flag-picker.js +104 -0
- package/workspace/public/pm/frame-copy.js +20 -1
- package/workspace/public/pm/panel-newsess.js +85 -3
- package/workspace/public/pm/tab-term.js +3 -0
- package/workspace/server.js +79 -11
- package/workspace/test-lists/workspace-launch-flags.json +15 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmos-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.97.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "pmos-workspace",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.97.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@xterm/addon-fit": "^0.11.0",
|
package/workspace/package.json
CHANGED
|
@@ -3381,6 +3381,28 @@
|
|
|
3381
3381
|
margin-left: -8px;
|
|
3382
3382
|
}
|
|
3383
3383
|
|
|
3384
|
+
/* launch-flag picker (flag-picker.js) and the profile form: new-session defaults on Settings, the flag list on the New session form */
|
|
3385
|
+
.fr-fp { display: flex; flex-direction: column; gap: 8px; min-width: 0; width: 100%; }
|
|
3386
|
+
.fr-fp-chips { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
3387
|
+
.fr-fp-chips .fr-fp-chip { font-family: var(--mono); font-weight: 400; }
|
|
3388
|
+
.fr-fp-chips .fr-fp-chip[aria-pressed="true"] { background: var(--accent); border-color: var(--foreground); }
|
|
3389
|
+
.fr-fp-list { max-height: 320px; overflow: auto; border: 1px solid var(--border); border-radius: 8px; background: var(--background); }
|
|
3390
|
+
.fr-fp-row { display: flex; flex-direction: column; gap: 2px; padding: 8px 10px; border-bottom: 1px solid var(--border); min-width: 0; }
|
|
3391
|
+
.fr-fp-row:last-child { border-bottom: 0; }
|
|
3392
|
+
.fr-fp-row--off { opacity: 0.55; }
|
|
3393
|
+
.fr-fp-head { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; font-family: var(--mono); font-size: 12px; }
|
|
3394
|
+
.fr-fp-val { display: flex; flex-direction: column; gap: 6px; padding-left: 24px; max-width: 420px; }
|
|
3395
|
+
.fr-fp-stack { display: flex; flex-direction: column; gap: 8px; }
|
|
3396
|
+
.fr-fp-sub { font-size: 12px; color: var(--muted-foreground); line-height: 1.5; }
|
|
3397
|
+
.fr-fp-label { font-family: var(--mono); font-size: 11px; font-weight: 500; letter-spacing: 0.1em; text-transform: uppercase; color: var(--muted-foreground); }
|
|
3398
|
+
.fr-fp-warn { font-size: 12px; line-height: 1.5; color: var(--warning); overflow-wrap: anywhere; }
|
|
3399
|
+
.fr-fp-defrow { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
|
|
3400
|
+
.fr-fp-note { margin: 0; color: var(--muted-foreground); font-size: 12px; line-height: 1.5; overflow-wrap: anywhere; }
|
|
3401
|
+
.fr-fp-bad { color: var(--destructive); }
|
|
3402
|
+
.fr-set__form { display: flex; flex-direction: column; gap: 10px; padding: 10px; border: 1px solid var(--border); border-radius: 8px; background: var(--background); }
|
|
3403
|
+
.fr-set__envrows { display: flex; flex-direction: column; gap: 6px; }
|
|
3404
|
+
.fr-set__flags { display: flex; flex-direction: column; gap: 8px; align-items: flex-start; min-width: 0; width: 100%; }
|
|
3405
|
+
|
|
3384
3406
|
/* -- layouts · .td- Today (Run 3, slice 3B) -- */
|
|
3385
3407
|
.td-board {
|
|
3386
3408
|
display: flex;
|
|
@@ -1237,6 +1237,12 @@ export async function loadProfiles() {
|
|
|
1237
1237
|
return getJson('/api/profiles');
|
|
1238
1238
|
}
|
|
1239
1239
|
|
|
1240
|
+
/** Every flag `claude --help` lists, each marked `offered` or not with the reason. A static file on the server. */
|
|
1241
|
+
export async function loadClaudeFlags() {
|
|
1242
|
+
if (FIXTURE) return { ok: false, error: 'fixture mode reads no flag list', flags: [] };
|
|
1243
|
+
return getJson('/api/claude-flags');
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1240
1246
|
/** Remember the profile this agent should preselect next time. The server refuses a preference for
|
|
1241
1247
|
* a descriptor it cannot read, so a stored id always resolves to a chip that is on screen. */
|
|
1242
1248
|
export async function preferProfile(agent, profile) {
|
|
@@ -32,6 +32,7 @@ import { sessionDefaults, setSessionDefaults } from './dock.js';
|
|
|
32
32
|
import { COPY } from './frame-copy.js';
|
|
33
33
|
import { askConfirm } from './dialogs.js';
|
|
34
34
|
import { icon } from './icons.js';
|
|
35
|
+
import { flagPicker, flagsLine, flagsMissingValue } from './flag-picker.js';
|
|
35
36
|
|
|
36
37
|
const P = COPY.page;
|
|
37
38
|
const roots = new WeakMap(); // host -> { root, sig }
|
|
@@ -178,13 +179,84 @@ function profilesSection() {
|
|
|
178
179
|
icon: 'ellipsis', ariaLabel: P.more(P.profilesRow), attrs: { 'data-fr': 'profiles-more' },
|
|
179
180
|
onClick: () => menu({ anchor: more, ariaLabel: P.more(P.profilesRow), items: [{ label: P.profilesOpen, onSelect: open }] }),
|
|
180
181
|
});
|
|
181
|
-
|
|
182
|
+
loadSessionReads();
|
|
183
|
+
const found = sess.profiles && Array.isArray(sess.profiles.profiles) ? sess.profiles.profiles : [];
|
|
184
|
+
const kids = [row({
|
|
182
185
|
title: P.profilesRow, meta: [P.profilesMeta], onOpen: open, openLabel: P.profilesOpen, trail: [more],
|
|
183
186
|
attrs: { 'data-fr-row': 'profiles' },
|
|
184
|
-
})]
|
|
187
|
+
})];
|
|
188
|
+
if (sess.formOpen) kids.push(profileForm());
|
|
189
|
+
if (sess.msg) kids.push(el('p', { class: 'fr-set__note', role: 'status', 'data-fr': 'profiles-msg' }, sess.msg));
|
|
190
|
+
kids.push(el('p', { class: 'fr-set__note', 'data-fr': 'profiles-found' }, !sess.profiles ? P.profilesReading
|
|
191
|
+
: sess.profiles.ok === false ? P.profilesUnread(sess.profiles.error)
|
|
192
|
+
: found.length ? P.profilesFound(found.map((x) => x.id)) : P.profilesNone(sess.profiles.dir)));
|
|
193
|
+
const add = button({ label: P.profileNew, variant: 'outline', size: 'sm', icon: 'plus', attrs: { 'data-fr': 'profile-new', 'aria-expanded': sess.formOpen ? 'true' : 'false' },
|
|
194
|
+
onClick: () => { sess.formOpen = !sess.formOpen; sess.msg = ''; setState({}); } });
|
|
195
|
+
return section('profiles', P.profiles, kids, add, 'bot');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/* ── the Claude profile and launch flags a new session starts with (workspace-launch-flags) ───────────────────────── */
|
|
199
|
+
const sess = { profiles: null, flags: null, asked: false, flagsOpen: false, filter: '', formOpen: false, msg: '' };
|
|
200
|
+
|
|
201
|
+
/** Both reads once per page load; a created profile asks for the profile read again. */
|
|
202
|
+
function loadSessionReads() {
|
|
203
|
+
if (FIXTURE) return;
|
|
204
|
+
if (!sess.asked) {
|
|
205
|
+
sess.asked = true;
|
|
206
|
+
pmGet('/api/claude-flags').then((j) => { sess.flags = j || { ok: false, error: 'no answer', flags: [] }; setState({}); });
|
|
207
|
+
}
|
|
208
|
+
if (!sess.profiles && !sess.reading) {
|
|
209
|
+
sess.reading = true;
|
|
210
|
+
pmGet('/api/profiles').then((j) => { sess.reading = false; sess.profiles = j || { ok: false, error: 'no answer', profiles: [] }; setState({}); });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
let profFormNode = null;
|
|
215
|
+
/** Built once per opening and handed back on every render, so typing is never lost to a rebuild. */
|
|
216
|
+
function profileForm() {
|
|
217
|
+
if (profFormNode) return profFormNode;
|
|
218
|
+
const idF = field({ label: P.pfId, placeholder: 'client-acme', attrs: { 'data-fr': 'pf-id' } });
|
|
219
|
+
const labelF = field({ label: P.pfLabel, placeholder: 'Client Acme', attrs: { 'data-fr': 'pf-label' } });
|
|
220
|
+
const acctF = field({ label: P.pfAccount, placeholder: 'you@company.com', attrs: { 'data-fr': 'pf-account' } });
|
|
221
|
+
const mcpF = field({ label: P.pfMcp, placeholder: 'jira confluence slack', attrs: { 'data-fr': 'pf-mcp' } });
|
|
222
|
+
const envHost = el('div', { class: 'fr-set__envrows', 'data-fr': 'pf-env' });
|
|
223
|
+
const rows = [];
|
|
224
|
+
const addEnv = (name) => {
|
|
225
|
+
const n = field({ label: P.pfEnvName, hideLabel: true, placeholder: 'NAME', value: name || '', attrs: { 'data-fr': 'pf-env-name' } });
|
|
226
|
+
const v = field({ label: P.pfEnvValue, hideLabel: true, placeholder: P.pfEnvValuePh, attrs: { 'data-fr': 'pf-env-value', type: 'password', autocomplete: 'new-password' } });
|
|
227
|
+
rows.push({ n, v });
|
|
228
|
+
envHost.append(el('div', { class: 'fr-set__fields fr-set__fields--grid' }, n, v));
|
|
229
|
+
return v;
|
|
230
|
+
};
|
|
231
|
+
addEnv('CLAUDE_CONFIG_DIR');
|
|
232
|
+
const err = el('p', { class: 'fr-set__note fr-set__error', role: 'alert', 'data-fr': 'pf-error' });
|
|
233
|
+
const reset = () => { profFormNode = null; sess.formOpen = false; };
|
|
234
|
+
const submit = async () => {
|
|
235
|
+
err.textContent = '';
|
|
236
|
+
const env = {};
|
|
237
|
+
for (const r of rows) { const k = r.n.control.value.trim(); if (k) env[k] = r.v.control.value; }
|
|
238
|
+
const body = { id: idF.control.value.trim(), label: labelF.control.value.trim(), account: acctF.control.value.trim(),
|
|
239
|
+
mcp: mcpF.control.value.split(/[\s,]+/).filter(Boolean), env };
|
|
240
|
+
const j = await pmPost('/api/profiles/create', body);
|
|
241
|
+
if (!j || j.ok === false) { err.textContent = (j && j.error) || P.pfFailed; return; }
|
|
242
|
+
reset(); sess.profiles = null; sess.msg = P.pfCreated(j.profile.id, j.profile.env_count);
|
|
243
|
+
setState({});
|
|
244
|
+
};
|
|
245
|
+
profFormNode = el('form', { class: 'fr-set__form', 'data-fr': 'profile-form', onSubmit: (e) => { e.preventDefault(); submit(); } },
|
|
246
|
+
el('div', { class: 'fr-set__fields fr-set__fields--grid' }, idF, labelF, acctF, mcpF),
|
|
247
|
+
el('p', { class: 'fr-set__note' }, P.pfEnvHead), envHost,
|
|
248
|
+
el('div', { class: 'fr-set__fields' },
|
|
249
|
+
button({ label: P.pfAddVar, variant: 'ghost', size: 'sm', icon: 'plus', attrs: { type: 'button', 'data-fr': 'pf-add-var' }, onClick: () => { addEnv('').control.focus(); } }),
|
|
250
|
+
...['CLAUDE_CONFIG_DIR', 'ANTHROPIC_API_KEY', 'ANTHROPIC_BASE_URL'].map((nm) => button({ label: nm, variant: 'ghost', size: 'sm', attrs: { type: 'button' }, onClick: () => { addEnv(nm).control.focus(); } }))),
|
|
251
|
+
el('p', { class: 'fr-set__note' }, P.pfNote), err,
|
|
252
|
+
el('div', { class: 'fr-set__fields' },
|
|
253
|
+
button({ label: P.cancel, variant: 'outline', attrs: { type: 'button' }, onClick: () => { reset(); setState({}); } }),
|
|
254
|
+
button({ label: P.pfCreate, variant: 'primary', attrs: { type: 'submit', 'data-fr': 'pf-create' } })));
|
|
255
|
+
return profFormNode;
|
|
185
256
|
}
|
|
186
257
|
|
|
187
258
|
function sessionsSection() {
|
|
259
|
+
loadSessionReads();
|
|
188
260
|
const d = sessionDefaults();
|
|
189
261
|
const cli = field({
|
|
190
262
|
label: P.cli, kind: 'select', value: d.cli,
|
|
@@ -198,7 +270,32 @@ function sessionsSection() {
|
|
|
198
270
|
onCommit: (v) => { setSessionDefaults({ tier: v }); setState({}); },
|
|
199
271
|
attrs: { 'data-fr': 'default-model' },
|
|
200
272
|
});
|
|
201
|
-
|
|
273
|
+
// Only descriptors the reader accepted are offered; a saved default that no longer reads stays shown, marked, so it is not silently swapped
|
|
274
|
+
const live = sess.profiles && Array.isArray(sess.profiles.profiles) ? sess.profiles.profiles : [];
|
|
275
|
+
const stale = d.profile && !live.some((x) => x.id === d.profile);
|
|
276
|
+
const profile = field({
|
|
277
|
+
label: P.profileDefault, kind: 'select', value: d.profile || '',
|
|
278
|
+
options: [{ value: '', label: P.noProfile }, ...(stale ? [{ value: d.profile, label: P.profileMissing(d.profile) }] : []),
|
|
279
|
+
...live.map((x) => ({ value: x.id, label: (x.label && x.label !== x.id ? x.label + ' (' + x.id + ')' : x.id) }))],
|
|
280
|
+
onCommit: (v) => {
|
|
281
|
+
setSessionDefaults({ profile: v || null });
|
|
282
|
+
// the form's own per-agent memory follows, so both starts agree; a refusal is shown by the form itself
|
|
283
|
+
pmPost('/api/profiles/prefer', { agent: 'claude', profile: v || null });
|
|
284
|
+
setState({});
|
|
285
|
+
},
|
|
286
|
+
attrs: { 'data-fr': 'default-profile' },
|
|
287
|
+
});
|
|
288
|
+
const flags = d.flags;
|
|
289
|
+
const flagsBox = el('div', { class: 'fr-set__flags', 'data-fr': 'default-flags' },
|
|
290
|
+
el('span', { class: 'fr-fp-label' }, P.flagsDefault),
|
|
291
|
+
el('p', { class: 'fr-set__note', 'data-fr': 'flags-summary' }, flags.length ? 'claude ' + flagsLine(flags) : P.flagsNone),
|
|
292
|
+
sess.flags && flagsMissingValue(sess.flags, flags).length ? el('p', { class: 'fr-set__note fr-set__error', role: 'alert', 'data-fr': 'flags-missing' }, P.flagsMissing(flagsMissingValue(sess.flags, flags)[0])) : null,
|
|
293
|
+
flagPicker({ list: sess.flags, chosen: flags, filter: sess.filter, compact: !sess.flagsOpen,
|
|
294
|
+
onChange: (next) => { setSessionDefaults({ flags: next }); setState({}); },
|
|
295
|
+
onFilter: (q) => { sess.filter = q; setState({}); } }),
|
|
296
|
+
button({ label: sess.flagsOpen ? P.flagsHide : P.flagsAll, variant: 'outline', size: 'sm', attrs: { 'data-fr': 'flags-all', 'aria-expanded': sess.flagsOpen ? 'true' : 'false' },
|
|
297
|
+
onClick: () => { sess.flagsOpen = !sess.flagsOpen; setState({}); } }));
|
|
298
|
+
return section('sessions', P.sessions, [el('div', { class: 'fr-set__fields fr-set__fields--grid' }, cli, model, profile), flagsBox, el('p', { class: 'fr-set__note' }, P.sessionsNote)], null, 'square-terminal');
|
|
202
299
|
}
|
|
203
300
|
|
|
204
301
|
function validateSection(scope) {
|
|
@@ -410,7 +507,7 @@ function signature(scope) {
|
|
|
410
507
|
let add = '';
|
|
411
508
|
try { add = JSON.stringify(addKeys); } catch { add = String(Date.now()); }
|
|
412
509
|
const kit = ws ? kitByWs.get(ws) : null;
|
|
413
|
-
return JSON.stringify([repoTuples(), activeProduct(), scope, state.theme, sessionDefaults(), ws, consentByWs.get(ws) || null, peopleByWs.get(ws) || null, addOpen, !!addMod, online.data, online.error, online.note,
|
|
510
|
+
return JSON.stringify([repoTuples(), activeProduct(), scope, state.theme, sessionDefaults(), sess.profiles, sess.flags, sess.flagsOpen, sess.filter, sess.formOpen, sess.msg, ws, consentByWs.get(ws) || null, peopleByWs.get(ws) || null, addOpen, !!addMod, online.data, online.error, online.note,
|
|
414
511
|
kit ? kit.st : null, upd]) + add;
|
|
415
512
|
}
|
|
416
513
|
|
|
@@ -299,7 +299,10 @@ export function openNewSessionSheet(repo, preset) {
|
|
|
299
299
|
const base = (sk ? (sk.label || sk.skill || sk.id) : (d.cli === 'claude' ? 'Claude' : 'Shell')) + (m.initiative ? ' · ' + m.initiative : '');
|
|
300
300
|
const name = typed || uniqueName(base);
|
|
301
301
|
const common = {
|
|
302
|
-
name, agent: sk ? 'claude' : d.cli, initiative: m.initiative || null,
|
|
302
|
+
name, agent: sk ? 'claude' : d.cli, initiative: m.initiative || null,
|
|
303
|
+
// the Settings defaults for a Claude session (workspace-launch-flags); a shell takes neither
|
|
304
|
+
profile: (sk || d.cli === 'claude') ? d.profile : null,
|
|
305
|
+
flags: !sk && d.cli === 'claude' && d.flags.length ? d.flags.map((x) => ({ flag: x.flag, ...(x.value ? { value: x.value } : {}) })) : undefined,
|
|
303
306
|
skill: sk ? sk.id : null, workType: sk ? typeOfSkill(sk) : 'general',
|
|
304
307
|
tier: !sk && d.cli === 'claude' && d.tier !== 'auto' ? d.tier : null,
|
|
305
308
|
};
|
|
@@ -33,17 +33,23 @@ const DEFAULTS_KEY = 'pmos.workspace.pm.session-defaults';
|
|
|
33
33
|
export const CLIS = Object.freeze(['shell', 'claude']);
|
|
34
34
|
export const TIERS = Object.freeze(['auto', 'fast', 'balanced', 'deep']);
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
/* The Claude profile and launch flags a new Claude session starts with (workspace-launch-flags). Stored beside the CLI and
|
|
37
|
+
* tier. A malformed entry is dropped, never repaired: the server checks every flag again against its own list. */
|
|
38
|
+
const cleanProfile = (v) => (typeof v === 'string' && /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(v) ? v : null);
|
|
39
|
+
const cleanFlags = (v) => (Array.isArray(v) ? v.filter((x) => x && typeof x.flag === 'string' && /^-{1,2}[A-Za-z][\w-]*$/.test(x.flag))
|
|
40
|
+
.map((x) => ({ flag: x.flag, value: typeof x.value === 'string' ? x.value.slice(0, 500) : '' })) : []);
|
|
41
|
+
|
|
42
|
+
/** { cli: 'shell'|'claude', tier: 'auto'|'fast'|'balanced'|'deep', profile: id|null, flags: [{flag,value}] }. An unreadable or unknown value is the default. */
|
|
37
43
|
export function sessionDefaults() {
|
|
38
44
|
let raw = null;
|
|
39
45
|
if (!FIXTURE) { try { raw = JSON.parse(localStorage.getItem(DEFAULTS_KEY) || 'null'); } catch { raw = null; } }
|
|
40
46
|
const r = raw && typeof raw === 'object' ? raw : {};
|
|
41
|
-
return { cli: CLIS.includes(r.cli) ? r.cli : 'shell', tier: TIERS.includes(r.tier) ? r.tier : 'auto' };
|
|
47
|
+
return { cli: CLIS.includes(r.cli) ? r.cli : 'shell', tier: TIERS.includes(r.tier) ? r.tier : 'auto', profile: cleanProfile(r.profile), flags: cleanFlags(r.flags) };
|
|
42
48
|
}
|
|
43
49
|
|
|
44
50
|
export function setSessionDefaults(patch) {
|
|
45
51
|
const next = { ...sessionDefaults(), ...(patch || {}) };
|
|
46
|
-
const clean = { cli: CLIS.includes(next.cli) ? next.cli : 'shell', tier: TIERS.includes(next.tier) ? next.tier : 'auto' };
|
|
52
|
+
const clean = { cli: CLIS.includes(next.cli) ? next.cli : 'shell', tier: TIERS.includes(next.tier) ? next.tier : 'auto', profile: cleanProfile(next.profile), flags: cleanFlags(next.flags) };
|
|
47
53
|
if (!FIXTURE) { try { localStorage.setItem(DEFAULTS_KEY, JSON.stringify(clean)); } catch { /* private mode: the default stands */ } }
|
|
48
54
|
return clean;
|
|
49
55
|
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/* pm/flag-picker.js — choose launch flags for a Claude session (initiative workspace-launch-flags).
|
|
2
|
+
*
|
|
3
|
+
* One picker, two places: Settings (the defaults every new session starts with) and the New session form (this
|
|
4
|
+
* session only). It draws the list the server serves at /api/claude-flags — every flag `claude --help` names — and
|
|
5
|
+
* holds NO state of its own: the caller keeps `chosen` ([{flag, value}]) and `filter`, passes them in, and redraws
|
|
6
|
+
* when `onChange` / `onFilter` fire. Text inputs commit on Enter or blur (the `field` primitive), so a redraw never
|
|
7
|
+
* lands between keystrokes and takes the caret with it.
|
|
8
|
+
*
|
|
9
|
+
* What a flag that takes a value offers, in order:
|
|
10
|
+
* choices `claude --help` states the complete set → a select, nothing else can be sent
|
|
11
|
+
* suggestions common values → chips to pick from, and the field still accepts another
|
|
12
|
+
* example free text, a path or JSON → the placeholder says what shape it is
|
|
13
|
+
* A flag PMOS answers itself (--model, --worktree …) or one that needs --print is listed, greyed, with the reason, and
|
|
14
|
+
* cannot be switched on. The server refuses the same flags, so this is a courtesy, not the guard.
|
|
15
|
+
*/
|
|
16
|
+
import { el } from './dom.js';
|
|
17
|
+
import { button, field, badge } from './primitives.js';
|
|
18
|
+
|
|
19
|
+
/** The flags worth one tap. Not a limit: every offered flag is in the list below them. */
|
|
20
|
+
export const QUICK_FLAGS = Object.freeze(['--chrome', '--continue', '--permission-mode', '--add-dir', '--verbose', '--bare']);
|
|
21
|
+
|
|
22
|
+
const find = (chosen, f) => (chosen || []).find((x) => x.flag === f);
|
|
23
|
+
|
|
24
|
+
/** `--chrome --permission-mode=plan`, or '' when nothing is chosen. */
|
|
25
|
+
export function flagsLine(chosen) {
|
|
26
|
+
return (chosen || []).map((x) => (x.value && String(x.value).trim() ? x.flag + '=' + String(x.value).trim() : x.flag)).join(' ');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The flags that need a value and have none yet: [flagName]. The session cannot start with these. */
|
|
30
|
+
export function flagsMissingValue(list, chosen) {
|
|
31
|
+
const by = new Map(((list && list.flags) || []).map((f) => [f.flag, f]));
|
|
32
|
+
return (chosen || []).filter((x) => by.get(x.flag) && by.get(x.flag).value === 'required' && !String(x.value || '').trim()).map((x) => x.flag);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** What goes on the wire: [{flag, value?}], or undefined for none. */
|
|
36
|
+
export function flagsWire(chosen) {
|
|
37
|
+
const out = (chosen || []).map((x) => ({ flag: x.flag, ...(x.value && String(x.value).trim() ? { value: String(x.value).trim() } : {}) }));
|
|
38
|
+
return out.length ? out : undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function valueEditor(f, cur, set) {
|
|
42
|
+
const label = 'Value for ' + f.flag;
|
|
43
|
+
if (Array.isArray(f.choices) && f.choices.length) {
|
|
44
|
+
return el('div', { class: 'fr-fp-val' },
|
|
45
|
+
field({ label, hideLabel: true, kind: 'select', value: cur.value || '', attrs: { 'data-fp-value': f.flag },
|
|
46
|
+
options: [{ value: '', label: 'Choose a value…' }, ...f.choices.map((c) => ({ value: c, label: c }))], onCommit: (v) => set(v) }),
|
|
47
|
+
el('span', { class: 'fr-fp-note' }, 'One of ' + f.choices.length + ', as `claude --help` lists them.'));
|
|
48
|
+
}
|
|
49
|
+
const sg = Array.isArray(f.suggestions) ? f.suggestions : [];
|
|
50
|
+
return el('div', { class: 'fr-fp-val' },
|
|
51
|
+
field({ label, hideLabel: true, value: cur.value || '', placeholder: f.example || f.arg || 'value', attrs: { 'data-fp-value': f.flag }, onCommit: (v) => set(v.trim()) }),
|
|
52
|
+
sg.length ? el('div', { class: 'fr-fp-chips', role: 'group', 'aria-label': 'Suggested values for ' + f.flag },
|
|
53
|
+
sg.map((v) => button({ label: v, variant: 'outline', size: 'sm', attrs: { class: 'fr-fp-chip', 'aria-pressed': v === cur.value ? 'true' : 'false', 'data-fp-suggest': f.flag }, onClick: () => set(v) }))) : null,
|
|
54
|
+
el('span', { class: 'fr-fp-note' }, sg.length ? 'Suggestions. You can type another value.' : 'Free text' + (f.arg ? ' ' + f.arg : '') + '.'));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @param {{list:{ok:boolean,error?:string,flags:object[]}|null, chosen:Array, filter:string, onChange:(next:Array)=>void,
|
|
59
|
+
* onFilter:(q:string)=>void, quick?:boolean}} o
|
|
60
|
+
*/
|
|
61
|
+
export function flagPicker({ list, chosen, filter, onChange, onFilter, quick = true, compact = false }) {
|
|
62
|
+
const root = el('div', { class: 'fr-fp', 'data-fp': 'root' });
|
|
63
|
+
if (!list) { root.append(el('p', { class: 'fr-fp-note' }, 'Reading the flag list…')); return root; }
|
|
64
|
+
if (list.ok === false) {
|
|
65
|
+
root.append(el('p', { class: 'fr-fp-note fp-bad', role: 'alert' }, 'The flag list could not be read — ' + (list.error || 'no reason given') + '. No flag can be added.'));
|
|
66
|
+
return root;
|
|
67
|
+
}
|
|
68
|
+
const all = list.flags || [];
|
|
69
|
+
const toggle = (f, on) => {
|
|
70
|
+
const cur = (chosen || []).filter((x) => x.flag !== f.flag);
|
|
71
|
+
onChange(on ? [...cur, { flag: f.flag, value: '' }] : cur);
|
|
72
|
+
};
|
|
73
|
+
const setValue = (f, v) => onChange((chosen || []).map((x) => (x.flag === f.flag ? { ...x, value: v } : x)));
|
|
74
|
+
|
|
75
|
+
if (quick) {
|
|
76
|
+
const qs = QUICK_FLAGS.map((n) => all.find((f) => f.flag === n)).filter((f) => f && f.offered);
|
|
77
|
+
root.append(el('div', { class: 'fr-fp-chips', role: 'group', 'aria-label': 'Common flags' },
|
|
78
|
+
qs.map((f) => button({ label: f.flag, variant: 'outline', size: 'sm', attrs: { class: 'fr-fp-chip', 'aria-pressed': find(chosen, f.flag) ? 'true' : 'false', 'data-fp-quick': f.flag }, onClick: () => toggle(f, !find(chosen, f.flag)) }))));
|
|
79
|
+
}
|
|
80
|
+
if (compact) {
|
|
81
|
+
// only what is switched on gets an editor; the full list opens on request
|
|
82
|
+
for (const f of all.filter((x) => find(chosen, x.flag) && x.value !== 'none')) root.append(el('div', { class: 'fr-fp-row', 'data-fp-row': f.flag }, el('code', { class: 'fr-fp-head' }, f.flag + (f.arg ? ' ' + f.arg : '')), valueEditor(f, find(chosen, f.flag), (v) => setValue(f, v))));
|
|
83
|
+
return root;
|
|
84
|
+
}
|
|
85
|
+
root.append(field({ label: 'Search flags', hideLabel: true, value: filter || '', placeholder: 'Search flags, e.g. chrome, permission, mcp (Enter)', attrs: { 'data-fp-filter': '1' }, onCommit: (v) => onFilter(v) }));
|
|
86
|
+
|
|
87
|
+
const q = String(filter || '').trim().toLowerCase();
|
|
88
|
+
const rows = all.filter((f) => !q || (f.flag + ' ' + (f.aliases || []).join(' ') + ' ' + f.description).toLowerCase().includes(q))
|
|
89
|
+
// what is switched on comes first, so its value editor is not buried in a list of 64
|
|
90
|
+
.sort((a, b) => (find(chosen, b.flag) ? 1 : 0) - (find(chosen, a.flag) ? 1 : 0));
|
|
91
|
+
root.append(el('div', { class: 'fr-fp-list', 'data-fp': 'list' },
|
|
92
|
+
rows.length ? rows.map((f) => {
|
|
93
|
+
const cur = find(chosen, f.flag);
|
|
94
|
+
const box = field({ kind: 'check', label: f.flag + (f.arg ? ' ' + f.arg : ''), value: !!cur, attrs: { 'data-fp-flag': f.flag, ...(f.offered ? {} : { disabled: true }) }, onCommit: (on) => toggle(f, on) });
|
|
95
|
+
return el('div', { class: 'fr-fp-row' + (f.offered ? '' : ' fr-fp-row--off'), 'data-fp-row': f.flag },
|
|
96
|
+
el('div', { class: 'fr-fp-head' }, box,
|
|
97
|
+
(f.aliases || []).length ? el('span', { class: 'fr-fp-note' }, f.aliases.join(' ')) : null,
|
|
98
|
+
f.caution ? badge({ text: 'Skips safety checks', tone: 'warning' }) : null,
|
|
99
|
+
f.offered ? null : badge({ text: 'Not offered' })),
|
|
100
|
+
el('p', { class: 'fr-fp-note' }, f.offered ? f.description : f.why_not),
|
|
101
|
+
cur && f.value !== 'none' ? valueEditor(f, cur, (v) => setValue(f, v)) : null);
|
|
102
|
+
}) : el('p', { class: 'fr-fp-note' }, 'No flag matches “' + (filter || '') + '”.')));
|
|
103
|
+
return root;
|
|
104
|
+
}
|
|
@@ -164,7 +164,26 @@ export const COPY = {
|
|
|
164
164
|
modelFast: 'Fast',
|
|
165
165
|
modelBalanced: 'Balanced',
|
|
166
166
|
modelDeep: 'Deep',
|
|
167
|
-
sessionsNote: 'The dock starts new sessions with these defaults. A model
|
|
167
|
+
sessionsNote: 'The dock starts new sessions with these defaults. A model, a profile and launch flags apply only to a Claude session. The New session form can change them for one session.',
|
|
168
|
+
profileDefault: 'Default Claude profile',
|
|
169
|
+
noProfile: 'No profile',
|
|
170
|
+
profileMissing: (id) => id + ' (not found now)',
|
|
171
|
+
flagsDefault: 'Default launch flags',
|
|
172
|
+
flagsNone: 'None. Claude starts with its own defaults.',
|
|
173
|
+
flagsAll: 'All flags…',
|
|
174
|
+
flagsMissing: (f) => f + ' needs a value, or switch it off. A new session cannot start with it empty.',
|
|
175
|
+
flagsHide: 'Hide the flag list',
|
|
176
|
+
profileNew: 'New profile',
|
|
177
|
+
profilesReading: 'Reading ~/.claude/profiles…',
|
|
178
|
+
profilesUnread: (e) => 'The profiles could not be read — ' + (e || 'no reason given'),
|
|
179
|
+
profilesFound: (ids) => 'Found ' + ids.length + ': ' + ids.join(', ') + '.',
|
|
180
|
+
profilesNone: (dir) => 'No profile in ' + (dir || '~/.claude/profiles') + ' yet.',
|
|
181
|
+
pfId: 'Id (the file name)', pfLabel: 'Label', pfAccount: 'Account, shown as written and not checked', pfMcp: 'MCP server names, declared only',
|
|
182
|
+
pfEnvHead: 'Environment variables applied to the session',
|
|
183
|
+
pfEnvName: 'Variable name', pfEnvValue: 'Value', pfEnvValuePh: 'value (hidden)', pfAddVar: 'Add variable',
|
|
184
|
+
pfNote: 'The values are written to ~/.claude/profiles/<id>.json on this machine, are never shown again and are never sent to the browser. CLAUDE_CONFIG_DIR gives the profile its own Claude login and settings.',
|
|
185
|
+
pfCreate: 'Create profile', pfFailed: 'The profile was not created.', cancel: 'Cancel',
|
|
186
|
+
pfCreated: (id, n) => 'Created ~/.claude/profiles/' + id + '.json with ' + n + ' environment variable' + (n === 1 ? '' : 's') + '.',
|
|
168
187
|
validate: 'Validate consent',
|
|
169
188
|
validateOn: 'Validation is on for this repository.',
|
|
170
189
|
validateOff: 'Validation is off. Nothing from this repository is sent.',
|
|
@@ -34,6 +34,9 @@ import { state, setState, FIXTURE } from './state.js';
|
|
|
34
34
|
import * as FX from './fixture.js';
|
|
35
35
|
import { openTerm as openTermTab } from './panel-initiatives.js';
|
|
36
36
|
import { startHere } from './panel-trees.js';
|
|
37
|
+
import { sessionDefaults } from './dock.js';
|
|
38
|
+
import { button } from './primitives.js';
|
|
39
|
+
import { flagPicker, flagsLine, flagsMissingValue, flagsWire } from './flag-picker.js';
|
|
37
40
|
|
|
38
41
|
/* Both are another module's file, loaded lazily for the reason panel-tabs.js gives: a static import
|
|
39
42
|
* of a file that fails to load takes this whole module down with it, and a panel that cannot draw
|
|
@@ -124,6 +127,8 @@ export function openNewSession(slug, seed) {
|
|
|
124
127
|
// Cleared on the way IN as well as on the way out: a form that opened still showing the profile
|
|
125
128
|
// a previous, abandoned question had chosen would be proposing an answer nobody gave.
|
|
126
129
|
preflightProfile: s.profile === undefined ? null : s.profile,
|
|
130
|
+
// the Settings defaults, for a Claude session (workspace-launch-flags)
|
|
131
|
+
newSessFlags: (s.agent || 'shell') === 'claude' ? sessionDefaults().flags.map((x) => ({ ...x })) : [], newSessFlagFilter: '',
|
|
127
132
|
});
|
|
128
133
|
refreshReads();
|
|
129
134
|
}
|
|
@@ -139,7 +144,7 @@ export function openNewSession(slug, seed) {
|
|
|
139
144
|
* other half is that `+ session` no longer reads this at all (panel-tabs.js).
|
|
140
145
|
*/
|
|
141
146
|
export function closeNewSession() {
|
|
142
|
-
setState({ newSess: null, newSessError: null, newSessBusy: false, preflightProfile: null });
|
|
147
|
+
setState({ newSess: null, newSessError: null, newSessBusy: false, preflightProfile: null, newSessFlags: [], newSessFlagFilter: '' });
|
|
143
148
|
}
|
|
144
149
|
|
|
145
150
|
export const isNewSessionOpen = () => !!state.newSess;
|
|
@@ -174,14 +179,18 @@ function refreshReads() {
|
|
|
174
179
|
if (typeof dataMod.loadBranches === 'function') {
|
|
175
180
|
dataMod.loadBranches(ws).then((r) => setState({ newSessBranches: r || null })).catch(() => {});
|
|
176
181
|
}
|
|
182
|
+
if (typeof dataMod.loadClaudeFlags === 'function') {
|
|
183
|
+
dataMod.loadClaudeFlags().then((r) => setState({ claudeFlags: r || null })).catch(() => {});
|
|
184
|
+
}
|
|
177
185
|
if (typeof dataMod.loadProfiles === 'function') {
|
|
178
186
|
dataMod.loadProfiles().then((r) => {
|
|
179
187
|
setState({ profiles: r || null });
|
|
180
188
|
// Preselect from the server's remembered preference — never from a hardcoded name. A
|
|
181
189
|
// preference naming a descriptor that no longer reads preselects NOTHING rather than
|
|
182
190
|
// substituting another, which is the same rule the server applies to the preference itself.
|
|
183
|
-
const pref = r && r.prefs ? r.prefs[state.newSessAgent] : null;
|
|
184
191
|
const live = (r && Array.isArray(r.profiles) ? r.profiles : []).map((p) => p && p.id);
|
|
192
|
+
const own = state.newSessAgent === 'claude' ? sessionDefaults().profile : null;
|
|
193
|
+
const pref = own && live.includes(own) ? own : (r && r.prefs ? r.prefs[state.newSessAgent] : null);
|
|
185
194
|
if (pref && live.includes(pref) && !state.preflightProfile) setState({ preflightProfile: pref });
|
|
186
195
|
}).catch(() => {});
|
|
187
196
|
}
|
|
@@ -377,6 +386,59 @@ function modelSection() {
|
|
|
377
386
|
: 'a ' + cur + ' tier — the model and effort it maps to are in your models policy')));
|
|
378
387
|
}
|
|
379
388
|
|
|
389
|
+
/* ── agent change ──────────────────────────────────────────────────────────────────────────────
|
|
390
|
+
* The remembered default is per agent, so a different agent preselects its own — but only when the PM has
|
|
391
|
+
* not chosen something else since the form opened. Launch flags belong to a claude session; leaving
|
|
392
|
+
* claude clears them rather than carrying them silently to a binary that may not know them. */
|
|
393
|
+
function defaultFor(agent) {
|
|
394
|
+
const p = state.profiles;
|
|
395
|
+
const live = (p && Array.isArray(p.profiles) ? p.profiles : []).map((x) => x && x.id);
|
|
396
|
+
// Settings holds the Claude default; the server's per-agent memory covers every other agent
|
|
397
|
+
const fromSettings = agent === 'claude' ? sessionDefaults().profile : null;
|
|
398
|
+
if (fromSettings && live.includes(fromSettings)) return fromSettings;
|
|
399
|
+
const pref = p && p.prefs ? p.prefs[agent] : null;
|
|
400
|
+
return pref && live.includes(pref) ? pref : null;
|
|
401
|
+
}
|
|
402
|
+
function pickAgent(a) {
|
|
403
|
+
const before = state.newSessAgent;
|
|
404
|
+
const untouched = !state.preflightProfile || state.preflightProfile === defaultFor(before);
|
|
405
|
+
setState({
|
|
406
|
+
newSessAgent: a, newSessError: null,
|
|
407
|
+
...(untouched ? { preflightProfile: defaultFor(a) } : {}),
|
|
408
|
+
...(a !== 'claude' ? { newSessFlags: [] } : (before !== 'claude' && !(state.newSessFlags || []).length ? { newSessFlags: sessionDefaults().flags.map((x) => ({ ...x })) } : {})),
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** Remember the chosen profile (or “no profile”) as this agent's default for the next form. */
|
|
413
|
+
async function setDefault() {
|
|
414
|
+
if (!dataMod || typeof dataMod.preferProfile !== 'function') return;
|
|
415
|
+
const r = await dataMod.preferProfile(state.newSessAgent || 'shell', state.preflightProfile === undefined ? null : state.preflightProfile);
|
|
416
|
+
if (!r || !r.ok) { setState({ newSessError: (r && r.error) || 'the default could not be saved' }); return; }
|
|
417
|
+
setState((s) => ({ newSessError: null, profiles: s.profiles ? { ...s.profiles, prefs: r.prefs || {} } : s.profiles }));
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/* ── the launch flags ───────────────────────────────────────────────────────────────────────────
|
|
421
|
+
* Every flag `claude --help` lists, from the server's generated file. A flag PMOS answers itself
|
|
422
|
+
* (--model, --worktree, --name …) or one that needs --print is shown, greyed, with the reason — never
|
|
423
|
+
* silently missing and never sendable. A flag that takes a value gets an input once it is switched on. */
|
|
424
|
+
function flagsSection() {
|
|
425
|
+
if (state.newSessAgent !== 'claude') {
|
|
426
|
+
return el('div', { 'data-flags': 'note', class: 'fr-fp-sub' }, iv('launch flags: only a Claude session takes flags — a shell, or any other CLI, starts as it is'));
|
|
427
|
+
}
|
|
428
|
+
const chosen = state.newSessFlags || [];
|
|
429
|
+
const r = state.claudeFlags;
|
|
430
|
+
const missing = r ? flagsMissingValue(r, chosen) : [];
|
|
431
|
+
return el('div', { class: 'fr-fp-stack', 'data-flags': 'section' },
|
|
432
|
+
el('div', { class: 'fr-fp-label' }, 'launch flags'),
|
|
433
|
+
el('div', { class: 'fr-fp-sub', 'data-flags': 'summary' }, iv(chosen.length
|
|
434
|
+
? 'will start with: claude ' + flagsLine(chosen)
|
|
435
|
+
: 'none chosen — claude starts with its own defaults' + (r && r.cli_version ? '. From claude ' + r.cli_version + ' --help.' : ''))),
|
|
436
|
+
missing.length ? el('div', { class: 'fr-fp-warn', 'data-flags': 'missing' }, iv(missing[0] + ' needs a value before the session can start.')) : null,
|
|
437
|
+
flagPicker({ list: r, chosen, filter: state.newSessFlagFilter,
|
|
438
|
+
onChange: (next) => setState({ newSessFlags: next, newSessError: null }),
|
|
439
|
+
onFilter: (q) => setState({ newSessFlagFilter: q }) }));
|
|
440
|
+
}
|
|
441
|
+
|
|
380
442
|
/* ── the profile chips ─────────────────────────────────────────────────────────────────────── */
|
|
381
443
|
|
|
382
444
|
/**
|
|
@@ -426,6 +488,21 @@ function profileSection() {
|
|
|
426
488
|
iv((b.file || '?').replace(/\.json$/, '') + ' — unusable'))),
|
|
427
489
|
));
|
|
428
490
|
|
|
491
|
+
{
|
|
492
|
+
const agent = state.newSessAgent || 'shell';
|
|
493
|
+
const def = defaultFor(agent);
|
|
494
|
+
const chosen = state.preflightProfile || null;
|
|
495
|
+
out.push(el('div', { class: 'fr-fp-defrow', 'data-profile': 'default' },
|
|
496
|
+
button({
|
|
497
|
+
label: chosen === def ? 'default for ' + agent : 'make ' + (chosen || 'no profile') + ' the default for ' + agent,
|
|
498
|
+
icon: chosen === def ? 'check' : undefined, size: 'sm', onClick: () => setDefault(), disabled: chosen === def,
|
|
499
|
+
attrs: { 'data-profile-default': 'set' },
|
|
500
|
+
}),
|
|
501
|
+
el('span', { class: 'fr-fp-sub' }, iv(def
|
|
502
|
+
? 'new ' + agent + ' sessions start with “' + def + '” unless you pick another here'
|
|
503
|
+
: 'no default is set — new ' + agent + ' sessions start with no profile'))));
|
|
504
|
+
}
|
|
505
|
+
|
|
429
506
|
if (p.absent) {
|
|
430
507
|
out.push(el('div', { style: SUB }, iv('no profile directory at ' + (p.dir || '?')
|
|
431
508
|
+ ' — nothing was read, so nothing is offered but “no profile”')));
|
|
@@ -490,6 +567,9 @@ async function onStart() {
|
|
|
490
567
|
return;
|
|
491
568
|
}
|
|
492
569
|
|
|
570
|
+
const missingVal = state.newSessAgent === 'claude' && state.claudeFlags ? flagsMissingValue(state.claudeFlags, state.newSessFlags) : [];
|
|
571
|
+
if (missingVal.length) { setState({ newSessError: missingVal[0] + ' needs a value, or switch it off — nothing was started' }); return; }
|
|
572
|
+
|
|
493
573
|
setState({ newSessBusy: true, newSessError: null });
|
|
494
574
|
const res = await termMod.requestSpawn({
|
|
495
575
|
name,
|
|
@@ -503,6 +583,7 @@ async function onStart() {
|
|
|
503
583
|
isolate: branch ? isolateNow() : null,
|
|
504
584
|
profile: state.preflightProfile === undefined ? null : state.preflightProfile,
|
|
505
585
|
// D75 — only for a Claude session; 'auto' sends nothing and lets the policy route.
|
|
586
|
+
flags: state.newSessAgent === 'claude' ? flagsWire(state.newSessFlags) : undefined,
|
|
506
587
|
tier: (state.newSessAgent === 'claude' && state.newSessTier && state.newSessTier !== 'auto') ? state.newSessTier : undefined,
|
|
507
588
|
});
|
|
508
589
|
|
|
@@ -584,13 +665,14 @@ export function newSessionBlock() {
|
|
|
584
665
|
el('div', { style: LABEL }, 'agent'),
|
|
585
666
|
el('div', { style: ROW },
|
|
586
667
|
['shell', 'claude'].map((a) => el('button', {
|
|
587
|
-
onClick: () =>
|
|
668
|
+
onClick: () => pickAgent(a),
|
|
588
669
|
style: state.newSessAgent === a ? CHIP_ON : CHIP, hover: HOVER,
|
|
589
670
|
}, iv(a))),
|
|
590
671
|
el('span', { style: SUB }, 'a CLI that is not on PATH is refused by the server, and says so'),
|
|
591
672
|
),
|
|
592
673
|
|
|
593
674
|
modelSection(),
|
|
675
|
+
el('div', { class: 'fr-fp-stack', 'data-r29': 'newsess-flags' }, flagsSection()),
|
|
594
676
|
el('div', { style: 'display:flex;flex-direction:column;gap:8px', 'data-r29': 'newsess-where' }, branchSection()),
|
|
595
677
|
el('div', { style: 'display:flex;flex-direction:column;gap:8px', 'data-r29': 'newsess-profile' }, profileSection()),
|
|
596
678
|
|
|
@@ -252,6 +252,7 @@ export function requestSpawn(opts) {
|
|
|
252
252
|
...(o.skill ? { skill: o.skill } : {}),
|
|
253
253
|
...(o.workType ? { workType: o.workType } : {}),
|
|
254
254
|
...(o.tier ? { tier: o.tier } : {}),
|
|
255
|
+
...(Array.isArray(o.flags) && o.flags.length ? { flags: o.flags } : {}),
|
|
255
256
|
} : {
|
|
256
257
|
t: 'new',
|
|
257
258
|
...(o.skill ? { skill: o.skill } : {}),
|
|
@@ -272,6 +273,8 @@ export function requestSpawn(opts) {
|
|
|
272
273
|
profile: o.profile === undefined ? null : o.profile,
|
|
273
274
|
// D75 — a tier the PM picked (fast | balanced | deep). Absent = "auto": the server routes by policy, or asks for nothing.
|
|
274
275
|
...(o.tier ? { tier: o.tier } : {}),
|
|
276
|
+
// workspace-launch-flags — `[{flag, value?}]`, checked by the server against the generated claude flag list.
|
|
277
|
+
...(Array.isArray(o.flags) && o.flags.length ? { flags: o.flags } : {}),
|
|
275
278
|
},
|
|
276
279
|
};
|
|
277
280
|
SOCK.spawnQueue.push(req);
|