create-pmos 0.214.0 → 0.216.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.
@@ -405,6 +405,10 @@
405
405
  .c3 .cft .cm{display:inline-flex;gap:6px;align-items:center}
406
406
  .c3 .cft .cm .conf{width:56px}
407
407
  .c3 .qd{border-left:2px solid var(--ring);padding:2px 0 2px 10px;font-size:var(--fs-3);white-space:pre-wrap;margin:0}
408
+ .c3 .qd.qd-md{white-space:normal;max-height:28rem;overflow:auto}
409
+ .c3 .wk-agent-md{font-size:var(--fs-3);max-width:none}
410
+ .c3 .wk-agent-md h1{font-size:var(--fs-4)}
411
+ .c3 .wk-agent-md h2{font-size:var(--fs-4);margin:14px 0 4px}
408
412
  .c3 .fun{display:grid;gap:4px}
409
413
  .c3 .fun div{display:grid;grid-template-columns:62px 1fr 34px;gap:8px;align-items:center;font:var(--fs-2) var(--mono);color:var(--muted-foreground)}
410
414
  .c3 .fun i{display:block;height:8px;border-radius:4px;background:var(--ring);transform-origin:left;animation:c3-grow .7s backwards}
@@ -1689,6 +1693,66 @@
1689
1693
  /* terminal sheet sizes, as the classic dock has them; the phone sheet is already full width */
1690
1694
  .c3 .c3t-size{flex:none}
1691
1695
  .c3 .c3t-size button{padding:4px 8px}
1696
+ /* the session sheet (c3-session-term.js): Conversation | Terminal, a list of running sessions at 2/3 and Whole, and the conversation (c3-session-conv.js) */
1697
+ .c3 .c3t-views button .ic{flex:none}
1698
+ .c3 .c3t-cv{display:grid;grid-template-columns:minmax(0,1fr);height:calc(100vh - 150px);min-height:240px;min-width:0}
1699
+ .c3 .c3t-cv-panes{display:flex;flex-direction:column;min-height:0;min-width:0}
1700
+ .c3 .c3t-cv-panes>[hidden]{display:none}
1701
+ .c3 .c3t-cv-panes .c3t-tw{flex:1 1 0;height:auto;min-height:0}
1702
+ .c3 .c3t-cv-list{display:none;border-right:1px solid var(--border);overflow:auto;padding:0 var(--sp-2) 0 0;margin-right:var(--sp-3);gap:2px;align-content:start}
1703
+ .c3 .c3t-cv-list .eyebrow{padding:var(--sp-2) 10px 4px;font:500 var(--fs-1) var(--mono);letter-spacing:.06em;text-transform:uppercase;color:var(--muted-foreground)}
1704
+ .c3 .c3t-cv-list button{display:grid;gap:2px;text-align:left;border:0;background:none;padding:8px 10px;border-radius:8px;cursor:pointer;color:var(--foreground);font:inherit;font-size:var(--fs-3);width:100%;min-width:0}
1705
+ .c3 .c3t-cv-list button .nm,.c3 .c3t-cv-list button .sub{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
1706
+ .c3 .c3t-cv-list button .sub{font:var(--fs-1) var(--mono);color:var(--muted-foreground)}
1707
+ .c3 .c3t-cv-list button[aria-current="true"]{background:color-mix(in oklab,var(--acc) 12%,transparent)}
1708
+ .c3 .c3t-cv-list button[aria-current="true"] .nm{color:var(--acc)}
1709
+ .c3 .c3t-cv-main{display:flex;flex-direction:column;flex:1 1 0;min-height:0;min-width:0}
1710
+ .c3 .c3t-cv-meta{display:flex;gap:var(--sp-2);align-items:center;flex-wrap:wrap;padding-bottom:var(--sp-2);border-bottom:1px solid var(--border);font:var(--fs-1) var(--mono);color:var(--muted-foreground)}
1711
+ .c3 .c3t-cv-meta .tg{--tc:var(--acc)}
1712
+ .c3 .c3t-cv-meta .tg[data-tone="muted"]{--tc:var(--muted-foreground)}
1713
+ .c3 .c3t-cv-meta .tg[data-tone="warning"]{--tc:var(--warning)}
1714
+ .c3 .c3t-cv-feed{flex:1 1 0;min-height:0;overflow:auto;padding:var(--sp-3) 2px}
1715
+ .c3 .c3t-cv-feed>.wrap{max-width:760px;margin:0 auto;display:flex;flex-direction:column;gap:var(--sp-2)}
1716
+ .c3 .c3t-cv .m{padding:8px 14px;border-radius:8px;max-width:85%;overflow-wrap:anywhere;font-size:var(--fs-3);line-height:1.5}
1717
+ .c3 .c3t-cv .m.me{align-self:flex-end;background:var(--acc);color:var(--on-accent);white-space:pre-wrap}
1718
+ .c3 .c3t-cv .m.bot{align-self:flex-start;background:var(--muted)}
1719
+ .c3 .c3t-cv .m.st{color:var(--muted-foreground);font:var(--fs-2) var(--mono);padding:0}
1720
+ .c3 .c3t-cv .m.bot .c3t-md{display:grid;gap:6px}
1721
+ .c3 .c3t-cv .m.bot .c3t-md>*{margin:0}
1722
+ .c3 .c3t-cv .m.bot ul,.c3 .c3t-cv .m.bot ol{padding-left:18px}
1723
+ .c3 .c3t-cv .m.bot code{font:var(--fs-2) var(--mono)}
1724
+ .c3 .c3t-cv .m.bot pre{margin:0;padding:8px 10px;border-radius:6px;background:var(--background);border:1px solid var(--border);overflow:auto;max-height:240px}
1725
+ .c3 .c3t-cv .who{display:flex;gap:6px;align-items:center;margin-top:var(--sp-2);font:500 var(--fs-1) var(--mono);color:var(--muted-foreground)}
1726
+ .c3 .c3t-cv .who.me{align-self:flex-end}
1727
+ .c3 .c3t-cv details.tools{align-self:flex-start;max-width:85%;font:var(--fs-2) var(--mono);color:var(--muted-foreground)}
1728
+ .c3 .c3t-cv details.tools summary{cursor:pointer;display:flex;gap:6px;align-items:center;list-style:none;padding:2px 0}
1729
+ .c3 .c3t-cv details.tools summary::-webkit-details-marker{display:none}
1730
+ .c3 .c3t-cv details.tools[open] summary .car{display:inline-flex;transform:rotate(90deg)}
1731
+ .c3 .c3t-cv details.tools ul{list-style:none;margin:4px 0 0;padding:0 0 0 18px;display:grid;gap:4px;border-left:2px solid var(--border)}
1732
+ .c3 .c3t-cv details.tools li{display:flex;gap:6px;align-items:baseline;min-width:0}
1733
+ .c3 .c3t-cv details.tools li b{color:var(--foreground);font-weight:500}
1734
+ .c3 .c3t-cv details.tools li span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}
1735
+ .c3 .c3t-cv .live{align-self:flex-start;display:flex;gap:8px;align-items:center;font:var(--fs-2) var(--mono);color:var(--muted-foreground)}
1736
+ .c3 .c3t-cv .perm{border:1px solid var(--warning);border-radius:8px;padding:var(--sp-3);display:grid;gap:var(--sp-2);background:color-mix(in oklab,var(--warning) 6%,var(--card))}
1737
+ .c3 .c3t-cv .perm .q{font:var(--fs-2) var(--mono)}
1738
+ .c3 .c3t-cv .perm .c3t-tray{display:flex;gap:var(--sp-2);flex-wrap:wrap;align-items:center}
1739
+ .c3 .c3t-cv .none{margin:var(--sp-4) auto;max-width:420px;text-align:center;display:grid;gap:var(--sp-3);justify-items:center}
1740
+ .c3 .c3t-cv .none>.ic{width:28px;height:28px;color:var(--muted-foreground)}
1741
+ .c3 .c3t-cv .none .dim{font-size:var(--fs-2)}
1742
+ .c3 .c3t-cv-comp{border-top:1px solid var(--border);padding-top:var(--sp-2);display:grid;gap:6px}
1743
+ .c3 .c3t-cv-comp[hidden],.c3 .c3t-cv-comp .box>[hidden]{display:none}
1744
+ .c3 .c3t-cv-comp .box{display:flex;gap:var(--sp-2);align-items:flex-end;border:1px solid var(--border);border-radius:8px;padding:6px 6px 6px 12px;background:var(--card)}
1745
+ .c3 .c3t-cv-comp .box:focus-within{border-color:var(--ring)}
1746
+ .c3 .c3t-cv-comp textarea{flex:1;border:0;background:none;resize:none;font:inherit;font-size:var(--fs-3);color:var(--foreground);padding:5px 0;outline:none;min-width:0}
1747
+ .c3 .c3t-cv-comp .hint{display:flex;gap:var(--sp-2);justify-content:space-between;align-items:center;flex-wrap:wrap;font:var(--fs-1) var(--mono);color:var(--muted-foreground)}
1748
+ .c3 .c3t-cv-comp .hint .chips2[hidden]{display:none}
1749
+ @media (min-width:761px){
1750
+ .c3:not(.ph) .sheet.c3t-ss.tsz-two-thirds .c3t-cv,.c3:not(.ph) .sheet.c3t-ss.tsz-whole .c3t-cv{grid-template-columns:220px minmax(0,1fr)}
1751
+ .c3:not(.ph) .sheet.c3t-ss.tsz-two-thirds .c3t-cv-list,.c3:not(.ph) .sheet.c3t-ss.tsz-whole .c3t-cv-list{display:grid}
1752
+ }
1753
+ .c3.ph .c3t-views .lb,.c3.ph .c3t-all .lb{display:none}
1754
+ .c3.ph .c3t-cv-comp .hint .kb{display:none}
1755
+ .c3.ph .c3t-cv .m,.c3.ph .c3t-cv details.tools{max-width:92%}
1692
1756
  .c3.ph .c3t-size{display:none}
1693
1757
  @media (min-width:761px){
1694
1758
  .c3 .sheet.wide.tsz-half{width:50vw}
@@ -1891,6 +1955,9 @@
1891
1955
  .c3 .wk-agent-sum{margin:0}
1892
1956
  .c3 .wk-agent-body{--tc:var(--acc)}
1893
1957
  .c3 .wk-agent-body .sit{white-space:pre-line}
1958
+ .c3 .wk-agent-doc .wk-agent-md{font-size:var(--fs-4);max-width:72ch}
1959
+ .c3 .wk-agent-editbox{display:grid;gap:var(--sp-2)}
1960
+ .c3 .wk-agent-edit{width:100%;box-sizing:border-box;font:inherit;font-size:var(--fs-2);line-height:1.5;padding:8px 10px;border:1px solid var(--border);background:var(--background);color:var(--foreground);border-radius:6px;resize:vertical}
1894
1961
  .c3 .tag.wk-off{background:var(--accw);color:var(--acc)}
1895
1962
  .c3 .tag.wk-nb{background:var(--muted);color:var(--foreground)}
1896
1963
  .c3 .tag.wk-on{background:var(--acc);color:var(--background)}
@@ -30,10 +30,11 @@ import { el } from './dom.js';
30
30
  import { button } from './primitives.js';
31
31
  import { pmGet, pmPost } from './http.js';
32
32
  import { renderCase } from './c3-case.js';
33
+ import { parseMd, toDom } from './c3-md.js';
33
34
  import { makeXp, attachExpand } from './c3-expand.js';
34
35
  import { attachSwipe } from './c3-swipe.js';
35
36
  import { toast } from './c3-toast.js';
36
- import { openSheet } from './c3-sheet.js';
37
+ import { openSheet, closeSheet } from './c3-sheet.js';
37
38
  import { c3btn } from './page-agent.js';
38
39
  import { onBriefing, loadBriefing } from './shell-ticker.js';
39
40
  import { reviewSection } from './c3-brief.js';
@@ -724,7 +725,9 @@ const AG = {
724
725
  openSession: 'Open the session',
725
726
  willDo: 'What the agent will do: ', change: 'Change what the agent does', askLabel: 'What the agent will do',
726
727
  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',
728
+ noteLabel: 'Tell the agent what to change', openDraft: 'Open the draft', editDraft: 'Edit the draft', edited: 'Edited by you. Post sends your text.',
729
+ saveDraft: 'Save the draft', cancel: 'Cancel', savedDraft: (w) => `Saved. Post sends your text to ${w}.`, savedMine: 'Saved. The draft stays here.', bodyLabel: 'The draft',
730
+ doneIt: 'Mark it done', doneFx: 'Closes the item. Nothing is posted', doneToast: 'Done. Nothing was posted.', draftSheet: (k) => 'Draft for ' + k, stop: 'stop',
728
731
  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
732
  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
733
  needNote: 'Say what to change first: type a note, then send it back.', link: 'Open what was posted',
@@ -785,6 +788,7 @@ function agentCase(inst, item, L) {
785
788
  const d = a.draft || {};
786
789
  quote = d.body || ''; sit = AG.sitReady('', w);
787
790
  if (d.summary) chg.push(['doc', d.summary, 'draft']);
791
+ if (d.edited_at) chg.push(['edit', AG.edited, '']);
788
792
  if (a.revisions) chg.push(['reply', 'Redrafted after your note' + (a.revisions > 1 ? 's (' + a.revisions + ')' : ''), '']);
789
793
  outbox = true; cta = 'Hold to post';
790
794
  const note = el('input', { type: 'text', placeholder: AG.noteLabel, 'aria-label': AG.noteLabel, 'data-pm-agent-note-in': item.id, autocomplete: 'off' });
@@ -794,16 +798,18 @@ function agentCase(inst, item, L) {
794
798
  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
799
  o('back', AG.back, [fx('reply', 'The agent drafts it again', 'ok')], back),
796
800
  o('keep', AG.keepDraft, [fx('check', 'Nothing leaves PMOS', 'ok')], () => say({ text: isMine(item) ? AG.keptMine : AG.kept, timeout: 3000 })),
801
+ o('done', AG.doneIt, [fx('check', AG.doneFx, 'ok')], (r) => doneAct(inst, item, r, AG.doneToast)),
797
802
  ].filter(Boolean);
798
803
  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))));
804
+ extras.push(el('div', { class: 'chips2' }, c3btn('chip2 lk', { 'data-pm-agent-draft': item.id }, [c3ic('eye', 'sm'), AG.openDraft], () => openDraft(item, inst)),
805
+ c3btn('chip2 lk', { 'data-pm-agent-edit': item.id }, [c3ic('edit', 'sm'), AG.editDraft], () => openDraft(item, inst, true))));
800
806
  after = noteBox;
801
807
  } else if (a.state === 'sending') {
802
808
  sit = AG.sitSending(k);
803
809
  } else if (a.state === 'sent') {
804
810
  sit = AG.sitSent(a.sent && a.sent.at ? hhmm(a.sent.at) : '', w);
805
811
  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)),
812
+ 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
813
  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
814
  }
809
815
  if (opts.length) opts[0].rec = 1;
@@ -817,21 +823,46 @@ function openAgentSession(id, item) {
817
823
  try { window.dispatchEvent(new CustomEvent('pmos:open-session', { detail: { id, name: 'Job · ' + (item.key || item.title || '') } })); } catch { /* no event */ }
818
824
  }
819
825
 
820
- function openDraft(item) {
826
+ /* The agent's draft drawn the way a document is (c3-md.js, as the document sheet draws it): headings, lists, tables, bold. */
827
+ const draftMd = (text) => el('div', { class: 'ds-md wk-agent-md' }, toDom(parseMd(String(text || '')), document));
828
+
829
+ /* 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
830
+ * as the draft, so Post sends exactly that text. */
831
+ function openDraft(item, inst, editing) {
821
832
  const a = item.agent || {}; const d = a.draft || {};
833
+ const canEdit = !!inst && a.state === 'ready';
834
+ const area = el('textarea', { class: 'wk-agent-edit', rows: '18', 'aria-label': AG.bodyLabel, 'data-pm-agent-edit-in': item.id });
835
+ area.value = d.body || '';
836
+ const save = async () => {
837
+ const v = area.value.trim();
838
+ if (!v) { say({ text: 'The draft cannot be empty. Send it back with a note instead.', timeout: 3500 }); area.focus(); return; }
839
+ const r = await pmPost('/api/my-work/decide', { item: item.id, action: 'edit', body: v });
840
+ if (!r || !r.ok) { say({ text: (r && r.error) || COPY.saveError, tone: 'error' }); return; }
841
+ closeSheet();
842
+ say({ text: isMine(item) ? AG.savedMine : AG.savedDraft(whereOf(item)), timeout: 4000 });
843
+ await load(inst);
844
+ };
845
+ const text = editing && canEdit
846
+ ? el('div', { class: 'wk-agent-editbox' }, area, el('div', { class: 'chips2' },
847
+ button({ label: AG.saveDraft, variant: 'primary', size: 'sm', onClick: save, attrs: { 'data-pm-agent-save': item.id } }),
848
+ button({ label: AG.cancel, variant: 'outline', size: 'sm', onClick: () => openDraft(item, inst, false) })))
849
+ : el('div', { class: 'wk-agent-doc' }, draftMd(d.body || ''));
822
850
  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),
851
+ 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
852
  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 || '')),
853
+ text,
826
854
  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 });
855
+ const actions = canEdit && !editing ? [c3btn('chip2 lk', { 'data-pm-agent-edit-sheet': item.id }, [c3ic('edit', 'sm'), AG.editDraft], () => openDraft(item, inst, true))] : null;
856
+ openSheet({ title: AG.draftSheet(item.key || item.title), icon: 'doc', body, actions });
857
+ if (editing && canEdit) { try { area.focus(); } catch { /* not focusable yet */ } }
828
858
  }
829
859
 
830
860
  /** 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). */
831
861
  function caseFile(inst, item, lens) {
832
862
  const { model, run } = itemCase(inst, item, lens);
833
863
  const root = renderCase(model, {
834
- picked: inst.picks.get(item.id), outbox: !!model.outbox,
864
+ picked: inst.picks.get(item.id), outbox: !!model.outbox, noConf: !item.mock_row,
865
+ renderQuote: item.agent && !item.mock_row ? draftMd : undefined,
835
866
  onPick: (id) => { inst.picks.set(item.id, id); const n = root.querySelector('[data-pm-agent-note]'); if (n) { n.hidden = id !== 'back'; if (!n.hidden) n.querySelector('input').focus(); } },
836
867
  onConfirm: ({ option }) => {
837
868
  const fn = run[option.id];
@@ -839,10 +870,6 @@ function caseFile(inst, item, lens) {
839
870
  },
840
871
  });
841
872
  root.setAttribute('data-pm-case', item.id);
842
- 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'); });
844
- root.querySelectorAll('.opt .conf').forEach((n) => n.remove());
845
- }
846
873
  const extras = [];
847
874
  const rs = inst.runs.get(item.id);
848
875
  const agent = item.mock_row && item.agent_move && (item.ix || {}).move;
@@ -863,7 +890,7 @@ function caseFile(inst, item, lens) {
863
890
  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
891
  c3ib('Close the reply', 'x', { onClick: () => { inst.trays.delete(item.id); draw(inst); } }, 'sm')));
865
892
  }
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')));
893
+ 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
894
  if (showSteps) extras.push(stepsBlock(inst, item));
868
895
  const anchor = root.querySelector('.c3ag') || root.querySelector('.sit');
869
896
  if (anchor) anchor.after(...extras); else root.prepend(...extras);
@@ -1200,10 +1227,9 @@ function jobCase(inst, j, close) {
1200
1227
  { id: 'mv', l: near ? 'Delete it and move its ' + its.length + ' item' + (its.length === 1 ? '' : 's') + ' to "' + nm(near) + '"' : 'Delete it', rec: 0, fx: [fx('trash', 'The job is gone', 'warn'), fx('target', its.length + ' items change job', 'warn')] },
1201
1228
  { id: 'nj', l: 'Delete it and leave its items without a job', rec: 0, fx: [fx('trash', 'The job is gone', 'warn'), fx('alert', 'The items have no job', '')] }] };
1202
1229
  const run = { keep: () => { say({ text: 'Kept.', timeout: 2500 }); close(); }, mv: del(near && near.id), nj: del(null) };
1203
- const root = renderCase(model, { onConfirm: ({ option }) => { if (run[option.id]) run[option.id](); } });
1230
+ // a job carries no confidence figure: never show a made-up percentage
1231
+ const root = renderCase(model, { noConf: true, onConfirm: ({ option }) => { if (run[option.id]) run[option.id](); } });
1204
1232
  root.setAttribute('data-pm-case', j.id);
1205
- root.querySelectorAll('.opt .rk').forEach((n) => { n.replaceChildren(c3ic('spark', 'sm'), 'Suggested'); n.removeAttribute('title'); }); // a job carries no confidence figure: never show a made-up percentage
1206
- root.querySelectorAll('.opt .conf').forEach((n) => n.remove());
1207
1233
  const sit = root.querySelector('.sit');
1208
1234
  sit.after(el('div', { class: 'c3jf' }, j.mock_name ? jobEditField(inst, j, 'mock_name') : null, jobEditField(inst, j, 'when'), jobEditField(inst, j, 'want'), jobEditField(inst, j, 'so')), jobAll(inst, j));
1209
1235
  return root;
@@ -1220,16 +1246,12 @@ function jobNewCase(inst, st, close) {
1220
1246
  const model = { id: 'job:new', sit: 'The agent drafted this from your line. Change anything, then create it. It has no items yet.', conf: 70, cta: 'Slide to create',
1221
1247
  chg: [['spark', 'Drafted by the agent', 'assumption', 'warn']],
1222
1248
  opts: [{ id: 'mk', l: 'Create the job', rec: 1, fx: [fx('target', 'Items can now be filed under it', 'ok')] }, { id: 'no', l: 'Not now', rec: 0, fx: [] }] };
1223
- const root = renderCase(model, { onConfirm: async ({ option }) => {
1249
+ const root = renderCase(model, { noConf: !demo, onConfirm: async ({ option }) => {
1224
1250
  if (option.id === 'no') { say({ text: 'Dropped the draft.', timeout: 2500 }); close(); return; }
1225
1251
  const line = 'when ' + val('when') + ', I want ' + val('want') + ', so ' + val('so');
1226
1252
  const r = await press(inst, JOBS + '/add', nameIn ? { line, name: nameIn.value.trim() } : { line });
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
- if (!demo) {
1230
- root.querySelectorAll('.opt .rk').forEach((n) => { n.replaceChildren(c3ic('spark', 'sm'), 'Suggested'); n.removeAttribute('title'); });
1231
- root.querySelectorAll('.opt .conf').forEach((n) => n.remove());
1232
- }
1233
1255
  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]))));
1234
1256
  return root;
1235
1257
  }
@@ -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,