create-pmos 0.214.0 → 0.215.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.
@@ -33,7 +33,7 @@ import { renderCase } from './c3-case.js';
33
33
  import { makeXp, attachExpand } from './c3-expand.js';
34
34
  import { attachSwipe } from './c3-swipe.js';
35
35
  import { toast } from './c3-toast.js';
36
- import { openSheet } from './c3-sheet.js';
36
+ import { openSheet, closeSheet } from './c3-sheet.js';
37
37
  import { c3btn } from './page-agent.js';
38
38
  import { onBriefing, loadBriefing } from './shell-ticker.js';
39
39
  import { reviewSection } from './c3-brief.js';
@@ -724,7 +724,9 @@ const AG = {
724
724
  openSession: 'Open the session',
725
725
  willDo: 'What the agent will do: ', change: 'Change what the agent does', askLabel: 'What the agent will do',
726
726
  draftIt: 'Let the agent draft this', tryAgain: 'Try again', post: (k) => 'Post it to ' + k, back: 'Send it back with a note', keepDraft: 'Keep it as a draft',
727
- noteLabel: 'Tell the agent what to change', openDraft: 'Open the draft', draftSheet: (k) => 'Draft for ' + k, stop: 'stop',
727
+ noteLabel: 'Tell the agent what to change', openDraft: 'Open the draft', editDraft: 'Edit the draft', edited: 'Edited by you. Post sends your text.',
728
+ saveDraft: 'Save the draft', cancel: 'Cancel', savedDraft: (w) => `Saved. Post sends your text to ${w}.`, savedMine: 'Saved. The draft stays here.', bodyLabel: 'The draft',
729
+ doneIt: 'Mark it done', doneFx: 'Closes the item. Nothing is posted', doneToast: 'Done. Nothing was posted.', draftSheet: (k) => 'Draft for ' + k, stop: 'stop',
728
730
  handed: (w) => `Handed to the agent. Nothing goes to ${w} until you post it.`, stopped: 'Stopped. Nothing was kept.', kept: 'Kept as a draft. Post it when you are ready.',
729
731
  posting: 'Approved. A session posts exactly this text.', sentBack: 'Sent back with your note. The agent drafts it again.', retried: 'The agent tries again.',
730
732
  needNote: 'Say what to change first: type a note, then send it back.', link: 'Open what was posted',
@@ -785,6 +787,7 @@ function agentCase(inst, item, L) {
785
787
  const d = a.draft || {};
786
788
  quote = d.body || ''; sit = AG.sitReady('', w);
787
789
  if (d.summary) chg.push(['doc', d.summary, 'draft']);
790
+ if (d.edited_at) chg.push(['edit', AG.edited, '']);
788
791
  if (a.revisions) chg.push(['reply', 'Redrafted after your note' + (a.revisions > 1 ? 's (' + a.revisions + ')' : ''), '']);
789
792
  outbox = true; cta = 'Hold to post';
790
793
  const note = el('input', { type: 'text', placeholder: AG.noteLabel, 'aria-label': AG.noteLabel, 'data-pm-agent-note-in': item.id, autocomplete: 'off' });
@@ -794,16 +797,18 @@ function agentCase(inst, item, L) {
794
797
  isMine(item) ? null : o('post', AG.post(k), [fx('lock', 'Outward: exactly this text', 'warn'), fx('out', 'Goes to ' + w + ' as you', 'warn')], () => agentPress(inst, '/api/my-work/decide', { item: item.id, action: 'approve' }, AG.posting), { outward: true }),
795
798
  o('back', AG.back, [fx('reply', 'The agent drafts it again', 'ok')], back),
796
799
  o('keep', AG.keepDraft, [fx('check', 'Nothing leaves PMOS', 'ok')], () => say({ text: isMine(item) ? AG.keptMine : AG.kept, timeout: 3000 })),
800
+ o('done', AG.doneIt, [fx('check', AG.doneFx, 'ok')], (r) => doneAct(inst, item, r, AG.doneToast)),
797
801
  ].filter(Boolean);
798
802
  const noteBox = el('div', { class: 'form wk-agent-ask', 'data-pm-agent-note': item.id }, note); noteBox.hidden = inst.picks.get(item.id) !== 'back';
799
- extras.push(el('div', { class: 'chips2' }, c3btn('chip2 lk', { 'data-pm-agent-draft': item.id }, [c3ic('eye', 'sm'), AG.openDraft], () => openDraft(item))));
803
+ extras.push(el('div', { class: 'chips2' }, c3btn('chip2 lk', { 'data-pm-agent-draft': item.id }, [c3ic('eye', 'sm'), AG.openDraft], () => openDraft(item, inst)),
804
+ c3btn('chip2 lk', { 'data-pm-agent-edit': item.id }, [c3ic('edit', 'sm'), AG.editDraft], () => openDraft(item, inst, true))));
800
805
  after = noteBox;
801
806
  } else if (a.state === 'sending') {
802
807
  sit = AG.sitSending(k);
803
808
  } else if (a.state === 'sent') {
804
809
  sit = AG.sitSent(a.sent && a.sent.at ? hhmm(a.sent.at) : '', w);
805
810
  opts = L.opts.map((x) => ({ ...x }));
806
- if (a.draft && a.draft.body) extras.push(el('div', { class: 'chips2' }, c3btn('chip2 lk', { 'data-pm-agent-draft': item.id }, [c3ic('eye', 'sm'), AG.openDraft], () => openDraft(item)),
811
+ if (a.draft && a.draft.body) extras.push(el('div', { class: 'chips2' }, c3btn('chip2 lk', { 'data-pm-agent-draft': item.id }, [c3ic('eye', 'sm'), AG.openDraft], () => openDraft(item, inst)),
807
812
  a.sent && a.sent.link ? el('a', { class: 'chip2 lk', href: a.sent.link, target: '_blank', rel: 'noopener noreferrer' }, c3ic('out', 'sm'), AG.link) : null));
808
813
  }
809
814
  if (opts.length) opts[0].rec = 1;
@@ -817,14 +822,35 @@ function openAgentSession(id, item) {
817
822
  try { window.dispatchEvent(new CustomEvent('pmos:open-session', { detail: { id, name: 'Job · ' + (item.key || item.title || '') } })); } catch { /* no event */ }
818
823
  }
819
824
 
820
- function openDraft(item) {
825
+ /* The agent's draft in a sheet. A draft that waits for the PM can be edited there (amendment 13): Save keeps the PM's text
826
+ * as the draft, so Post sends exactly that text. */
827
+ function openDraft(item, inst, editing) {
821
828
  const a = item.agent || {}; const d = a.draft || {};
829
+ const canEdit = !!inst && a.state === 'ready';
830
+ const area = el('textarea', { class: 'wk-agent-edit', rows: '18', 'aria-label': AG.bodyLabel, 'data-pm-agent-edit-in': item.id });
831
+ area.value = d.body || '';
832
+ const save = async () => {
833
+ const v = area.value.trim();
834
+ if (!v) { say({ text: 'The draft cannot be empty. Send it back with a note instead.', timeout: 3500 }); area.focus(); return; }
835
+ const r = await pmPost('/api/my-work/decide', { item: item.id, action: 'edit', body: v });
836
+ if (!r || !r.ok) { say({ text: (r && r.error) || COPY.saveError, tone: 'error' }); return; }
837
+ closeSheet();
838
+ say({ text: isMine(item) ? AG.savedMine : AG.savedDraft(whereOf(item)), timeout: 4000 });
839
+ await load(inst);
840
+ };
841
+ const text = editing && canEdit
842
+ ? el('div', { class: 'wk-agent-editbox' }, area, el('div', { class: 'chips2' },
843
+ button({ label: AG.saveDraft, variant: 'primary', size: 'sm', onClick: save, attrs: { 'data-pm-agent-save': item.id } }),
844
+ button({ label: AG.cancel, variant: 'outline', size: 'sm', onClick: () => openDraft(item, inst, false) })))
845
+ : el('div', { class: 'case wk-agent-body' }, el('p', { class: 'sit' }, d.body || ''));
822
846
  const body = el('div', { class: 'wk-agent-sheet' },
823
- el('div', { class: 'chips' }, el('span', { class: 'tag' }, a.state === 'sent' ? 'posted' : 'draft'), item.key ? el('span', { class: 'chip2' }, c3ic('org', 'sm'), (item.origin === 'jira' ? 'Jira ' : '') + item.key) : null),
847
+ el('div', { class: 'chips' }, el('span', { class: 'tag' }, a.state === 'sent' ? 'posted' : d.edited_at ? 'draft · edited by you' : 'draft'), item.key ? el('span', { class: 'chip2' }, c3ic('org', 'sm'), (item.origin === 'jira' ? 'Jira ' : '') + item.key) : null),
824
848
  d.summary ? el('p', { class: 'dim wk-agent-sum' }, d.summary) : null,
825
- el('div', { class: 'case wk-agent-body' }, el('p', { class: 'sit' }, d.body || '')),
849
+ text,
826
850
  el('div', { class: 'fold' }, c3ic('info', 'sm'), ' ' + (a.state === 'sent' ? 'This is the text that was posted.' : 'Nothing is in ' + whereOf(item) + ' yet. Post it from the item, after you read it.')));
827
- openSheet({ title: AG.draftSheet(item.key || item.title), icon: 'doc', body });
851
+ const actions = canEdit && !editing ? [c3btn('chip2 lk', { 'data-pm-agent-edit-sheet': item.id }, [c3ic('edit', 'sm'), AG.editDraft], () => openDraft(item, inst, true))] : null;
852
+ openSheet({ title: AG.draftSheet(item.key || item.title), icon: 'doc', body, actions });
853
+ if (editing && canEdit) { try { area.focus(); } catch { /* not focusable yet */ } }
828
854
  }
829
855
 
830
856
  /** The case file element of an item: c3-case.js plus what is Work's own (offline work, the shared actions, the steps, the agent tray, the edit fields). */
@@ -840,7 +866,7 @@ function caseFile(inst, item, lens) {
840
866
  });
841
867
  root.setAttribute('data-pm-case', item.id);
842
868
  if (!item.mock_row) { // real items carry no confidence figure: never show a made-up percentage
843
- root.querySelectorAll('.opt .rk').forEach((n) => { n.replaceChildren(c3ic('spark', 'sm'), 'Suggested'); n.removeAttribute('title'); });
869
+ root.querySelectorAll('.opt .rk').forEach((n) => { if (n.childNodes.length) n.replaceChildren(c3ic('spark', 'sm'), 'Suggested'); else n.remove(); n.removeAttribute('title'); });
844
870
  root.querySelectorAll('.opt .conf').forEach((n) => n.remove());
845
871
  }
846
872
  const extras = [];
@@ -863,7 +889,7 @@ function caseFile(inst, item, lens) {
863
889
  c3ib('Speak is off on this line: nothing on this page records. Type the reply here.', 'mic', { 'data-off': '', onclick: () => say({ text: 'The microphone needs the app. Nothing was recorded: type the reply here.', timeout: 4200 }) }, 'sm'),
864
890
  c3ib('Close the reply', 'x', { onClick: () => { inst.trays.delete(item.id); draw(inst); } }, 'sm')));
865
891
  }
866
- const showSteps = !(rs && rs.phase) && item.kind !== 'offline' && !(item.agent && !item.mock_row && item.agent.state !== 'sent' && !isMine(item)) && (inst.planning.has(item.id) || (item.steps || []).length || (!agent && (!item.mock_row || item.src === 'You')));
892
+ const showSteps = !(rs && rs.phase) && item.kind !== 'offline' && !(item.agent && !item.mock_row && item.agent.state !== 'sent' && !isMine(item) && !(item.steps || []).length) && (inst.planning.has(item.id) || (item.steps || []).length || (!agent && (!item.mock_row || item.src === 'You')));
867
893
  if (showSteps) extras.push(stepsBlock(inst, item));
868
894
  const anchor = root.querySelector('.c3ag') || root.querySelector('.sit');
869
895
  if (anchor) anchor.after(...extras); else root.prepend(...extras);
@@ -1227,7 +1253,7 @@ function jobNewCase(inst, st, close) {
1227
1253
  if (r) { say({ text: 'Job created. Add work to it by saying which job it serves.', timeout: 3000 }); close(); await load(inst); }
1228
1254
  } });
1229
1255
  if (!demo) {
1230
- root.querySelectorAll('.opt .rk').forEach((n) => { n.replaceChildren(c3ic('spark', 'sm'), 'Suggested'); n.removeAttribute('title'); });
1256
+ root.querySelectorAll('.opt .rk').forEach((n) => { if (n.childNodes.length) n.replaceChildren(c3ic('spark', 'sm'), 'Suggested'); else n.remove(); n.removeAttribute('title'); });
1231
1257
  root.querySelectorAll('.opt .conf').forEach((n) => n.remove());
1232
1258
  }
1233
1259
  root.querySelector('.sit').after(el('div', { class: 'c3jf' }, nameIn ? field('Job', nameIn) : null, ...['when', 'want', 'so'].map((k, i) => field(k === 'when' ? 'When' : k === 'want' ? 'I want' : 'So', inputs[i]))));
@@ -1098,6 +1098,15 @@ export async function typeIntoSession(id, text) {
1098
1098
  return send({ t: 'input', id, data: '\r' });
1099
1099
  }
1100
1100
 
1101
+ /** Keys typed into a session as they are, with no Enter after them: a menu choice ('1') or Esc ('\x1b') for a running agent.
1102
+ * The same frame and socket as typeIntoSession. Returns false when nothing was sent. */
1103
+ export async function pressKeys(id, data) {
1104
+ if (FIXTURE || !id || !data) return false;
1105
+ connect();
1106
+ for (let i = 0; i < 50 && !socketOpen(); i++) await new Promise((r) => setTimeout(r, 100));
1107
+ return send({ t: 'input', id, data });
1108
+ }
1109
+
1101
1110
  /**
1102
1111
  * End a running session, by id — the `+ session` chooser's one destructive action.
1103
1112
  *
@@ -2939,22 +2939,41 @@ function txFound(s, now = Date.now()) {
2939
2939
  }
2940
2940
  // console3's session conversation: the PM's prompts and the agent's words from the transcript (transcript-read.readConversation).
2941
2941
  // Re-parsed only when the file moves; `stamp` lets the page skip a redraw when nothing changed. Read-only and local.
2942
+ // `state` says what the page can show: 'conv' (a conversation), 'shell' (a shell with no agent in it yet), 'unlinked' (an agent
2943
+ // whose record cannot be found or read), 'other' (an agent that keeps no Claude transcript). `ask` is Claude Code's permission
2944
+ // prompt while it is on the pane's screen (permissionAsk).
2942
2945
  function sessionConversation(s) {
2943
- if (s.agent !== 'claude' && s.agent !== 'shell') return { messages: [], missing: 'Only Claude sessions show a conversation here.' };
2946
+ if (s.agent !== 'claude' && s.agent !== 'shell') return { messages: [], state: 'other', missing: 'Only Claude sessions show a conversation here.' };
2944
2947
  const found = txFound(s);
2945
2948
  if (found.missing) {
2946
- if (s.agent === 'shell' && /wrote no session record/.test(found.missing)) return { messages: [], missing: 'No agent is running in this shell yet.' };
2947
- return { messages: [], missing: found.missing };
2949
+ if (s.agent === 'shell' && /wrote no session record/.test(found.missing)) return { messages: [], state: 'shell', missing: 'No agent is running in this shell yet.' };
2950
+ return { messages: [], state: 'unlinked', missing: found.missing };
2948
2951
  }
2949
2952
  let stamp = null;
2950
2953
  try { const st = fs.statSync(found.file); stamp = `${st.size}:${st.mtimeMs}`; } catch {}
2951
- if (s.conv && s.convStamp === stamp) return s.conv;
2952
- const c = transcript.readConversation(found.file, scrub);
2953
- const h = transcriptHarness(s);
2954
- c.turn = h && h.turn ? h.turn : null;
2955
- c.stamp = stamp;
2956
- s.conv = c; s.convStamp = stamp;
2957
- return c;
2954
+ let c = s.conv;
2955
+ if (!(c && s.convStamp === stamp)) {
2956
+ c = transcript.readConversation(found.file, scrub, transcript.CONV_LIMIT, s.root);
2957
+ c.state = 'conv';
2958
+ c.stamp = stamp;
2959
+ s.conv = c; s.convStamp = stamp;
2960
+ }
2961
+ // whose turn comes from the harness read, which has its own re-read floor: taken on every call, so a turn that the floor
2962
+ // held back shows on the next poll even when the file has not moved since
2963
+ const h = transcriptHarness(s) || {};
2964
+ const out = { ...c, turn: h.turn || null, openCall: h.openCall ? h.openCall.verb : null, model: h.model || null };
2965
+ // the prompt is on the screen, not in the transcript: read it only while a tool call is open
2966
+ const ask = out.turn === 'tool' ? paneAsk(s) : null;
2967
+ return ask ? { ...out, ask } : out;
2968
+ }
2969
+ // The pane's visible screen, plain text, read at most once every 1.5s per session (a tmux capture is a blocking spawn).
2970
+ function paneAsk(s, now = Date.now()) {
2971
+ if (!s.tmux || !TMUX) return null;
2972
+ if (s.askAt && now - s.askAt < 1500) return s.ask || null;
2973
+ s.askAt = now;
2974
+ const r = tmuxCtl(['capture-pane', '-p', '-t', s.tmux]);
2975
+ s.ask = r.status === 0 && r.stdout ? transcript.permissionAsk(r.stdout, scrub) : null;
2976
+ return s.ask;
2958
2977
  }
2959
2978
  function transcriptHarness(s) {
2960
2979
  if (s.state === 'exited') return { missing: { all: 'the session has exited' } };
@@ -6682,9 +6701,10 @@ async function handleRequest(req, res) {
6682
6701
  if (p === '/api/sessions/conversation' && req.method === 'GET') {
6683
6702
  const sess = sessions.get(String(url.searchParams.get('id') || ''));
6684
6703
  if (!sess) return json(res, { ok: false, error: 'no such session' }, 404);
6685
- if (sess.state === 'exited') return json(res, { ok: true, id: sess.id, messages: [], missing: 'The session has ended.', turn: null, stamp: null });
6704
+ if (sess.state === 'exited') return json(res, { ok: true, id: sess.id, agent: sess.agent, messages: [], state: 'ended', missing: 'The session has ended.', turn: null, stamp: null });
6686
6705
  const c = sessionConversation(sess);
6687
- return json(res, { ok: true, id: sess.id, messages: c.messages, missing: c.missing || null, turn: c.turn || null, stamp: c.stamp || null });
6706
+ return json(res, { ok: true, id: sess.id, agent: sess.agent, messages: c.messages, state: c.state || null, missing: c.missing || null, turn: c.turn || null,
6707
+ openCall: c.openCall || null, model: c.model || null, ask: c.ask || null, stamp: c.stamp || null });
6688
6708
  }
6689
6709
  if (p === '/api/sessions/transcript' && req.method === 'GET') {
6690
6710
  const sess = sessions.get(String(url.searchParams.get('id') || ''));
@@ -36,6 +36,7 @@ Procedure:
36
36
  4. If MODE is send:
37
37
  a. The task is approved. Read the approved draft and its link in the output of `day show`. The approved draft is the only text you may send. Do not read it as a draft to improve.
38
38
  A draft whose summary starts "The PM's own" was written by the PM in the Your day card. Post it exactly as written, as the PM, to the place the summary and the link name. Do not add a greeting, a signature or a line of your own.
39
+ If `day show` prints "edited by the PM", the PM changed the draft in Work. The body is the PM's text. Post it exactly as written, the same way.
39
40
  b. Do the one outward step the summary names, with the connector that fits, to the place the link names. Examples: post the approved reply in the linked Slack thread, add the approved comment to the linked Jira issue, reply to the linked mail thread. If the summary names more than one step, do only the first and report the rest with `day fail`.
40
41
  c. If the place does not match the link, the thread is closed, or the connector is not available, do not try another route. Run `pmos proactive day fail TASK --reason "<what stopped it>"`.
41
42
  d. When it is done, run `pmos proactive day sent TASK --link <URL of what you posted or sent>`. Leave out `--link` only if the connector returns none.
@@ -299,7 +299,7 @@
299
299
  "pm-portfolio-inline-test.js": 15,
300
300
  "settings-store.test.js": 1,
301
301
  "c3-settings-test.js": 25,
302
- "c3-sessions-panel-test.js": 20,
302
+ "c3-sessions-panel-test.js": 70,
303
303
  "c3-updates-test.js": 20,
304
304
  "c3-quiet-test.js": 30,
305
305
  "c3-phone-model.test.js": 1,