@tuzelko/opencode-delayed-message 0.1.2 → 0.1.4

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/README.md CHANGED
@@ -85,9 +85,14 @@ Above the composer, while a message is pending:
85
85
  ⏰ Scheduled · 10:18
86
86
  ```
87
87
 
88
- Click it for the list: time, relative countdown, a preview of the text, and `×` to cancel.
89
- The pill disappears when the message is delivered or cancelled. The terminal UI has no pill —
90
- there the transcript note is the only trace.
88
+ Click it for the list: one line per message with the scheduled time, a preview, and
89
+ the countdown, like the app's own subagent list. `✎` to edit and `×` to cancel appear
90
+ when you hover a row. Edit cancels the pending job and puts the whole command back into
91
+ the composer — `/delayed-message`, the scheduled time, and the full text — with the
92
+ cursor in the prompt. Adjust anything and submit to reschedule. The pill disappears
93
+ when the message is delivered or cancelled.
94
+
95
+ The terminal UI has no pill — there the transcript note is the only trace.
91
96
 
92
97
  The pill follows the app language: English, Russian, German, French, and Chinese are
93
98
  included. Transcript notes are English, because they are written for the model.
@@ -113,8 +118,8 @@ waits for the next time that directory loads.
113
118
  24.8-day timer cap.
114
119
  - On fire: `ctx.session.prompt({ sessionID, text })` — a real user prompt, so the session
115
120
  wakes.
116
- - The GUI talks to the server through an RPC: `pending`, `cancel`, `deliver`, and the
117
- events `scheduled`, `fired`, `cancelled`.
121
+ - The GUI talks to the server through an RPC: `pending`, `cancel`, `edit`, `deliver`, and
122
+ the events `scheduled`, `fired`, `cancelled`.
118
123
  - The GUI main process mirrors `scheduled` jobs into its own storage and arms Node timers.
119
124
  When one is due it calls `deliver` with the job's directory, which boots the location
120
125
  and delivers the message even though the server-side timer is gone.
@@ -1,11 +1,13 @@
1
- Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:`Module`}});let e=require("solid-js/web"),t=require("solid-js"),n=require("effect"),r=require("@opencode/plugin/gui");var i=require("@opencode/plugin/rpc").Rpc.define({id:`delayed-message`,methods:{pending:{input:{type:`object`,properties:{sessionID:{type:`string`}},required:[`sessionID`],additionalProperties:!1},output:{type:`object`,properties:{jobs:{type:`array`,items:{type:`object`,properties:{id:{type:`string`},fireAt:{type:`number`},preview:{type:`string`}},required:[`id`,`fireAt`,`preview`],additionalProperties:!1}}},required:[`jobs`],additionalProperties:!1}},cancel:{input:{type:`object`,properties:{sessionID:{type:`string`},id:{type:`string`}},required:[`sessionID`,`id`],additionalProperties:!1},output:{type:`object`,properties:{ok:{type:`boolean`}},required:[`ok`],additionalProperties:!1}},deliver:{input:{type:`object`,properties:{sessionID:{type:`string`},id:{type:`string`}},required:[`sessionID`,`id`],additionalProperties:!1},output:{type:`object`,properties:{status:{type:`string`}},required:[`status`],additionalProperties:!1}}},events:{scheduled:{schema:{type:`object`,properties:{sessionID:{type:`string`},id:{type:`string`},fireAt:{type:`number`},preview:{type:`string`},directory:{type:`string`}},required:[`sessionID`,`id`,`fireAt`,`preview`,`directory`],additionalProperties:!1}},fired:{schema:{type:`object`,properties:{sessionID:{type:`string`},id:{type:`string`}},required:[`sessionID`,`id`],additionalProperties:!1}},cancelled:{schema:{type:`object`,properties:{sessionID:{type:`string`},id:{type:`string`}},required:[`sessionID`,`id`],additionalProperties:!1}}}}),a=(0,e.template)(`<div class=dm>`),o=(0,e.template)(`<div class=dm-row><div class=dm-body><div class=dm-when><span></span><span class=dm-left></span></div><div class=dm-text></div></div><button class=dm-x>×`),s=2**31-1,c=`
2
- .dm { display: flex; flex-direction: column; min-width: 280px; max-width: 440px; }
3
- .dm-row { display: flex; align-items: center; gap: 8px; padding: 8px 2px; }
4
- .dm-row + .dm-row { border-top: 1px solid color-mix(in srgb, currentColor 12%, transparent); }
5
- .dm-body { flex: 1; min-width: 0; }
6
- .dm-when { display: flex; align-items: baseline; gap: 8px; font-variant-numeric: tabular-nums; }
7
- .dm-left { opacity: .55; font-size: 12px; white-space: nowrap; }
8
- .dm-text { opacity: .72; font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
9
- .dm-x { flex: none; width: 22px; height: 22px; border: 0; border-radius: 6px; background: transparent; color: inherit; opacity: .45; cursor: pointer; font: inherit; line-height: 1; }
10
- .dm-x:hover { opacity: 1; background: color-mix(in srgb, currentColor 14%, transparent); }
11
- `,l=r.Plugin.define({i18n:{en:{"pill.one":`Scheduled · {{time}}`,"pill.many":`Scheduled: {{count}}`,"menu.title":`Scheduled messages`,"row.cancel":`Cancel`,"left.now":`now`,"left.days":`in {{days}}d {{hours}}h`,"left.hours":`in {{hours}}h {{minutes}}m`,"left.minutes":`in {{minutes}}m`},ru:{"pill.one":`Отложенное · {{time}}`,"pill.many":`Отложенных: {{count}}`,"menu.title":`Отложенные сообщения`,"row.cancel":`Отменить`,"left.now":`сейчас`,"left.days":`через {{days}} д {{hours}} ч`,"left.hours":`через {{hours}} ч {{minutes}} мин`,"left.minutes":`через {{minutes}} мин`},de:{"pill.one":`Geplant · {{time}}`,"pill.many":`Geplant: {{count}}`,"menu.title":`Geplante Nachrichten`,"row.cancel":`Abbrechen`,"left.now":`jetzt`,"left.days":`in {{days}} T {{hours}} Std.`,"left.hours":`in {{hours}} Std. {{minutes}} Min.`,"left.minutes":`in {{minutes}} Min.`},fr:{"pill.one":`Programmé · {{time}}`,"pill.many":`Programmés : {{count}}`,"menu.title":`Messages programmés`,"row.cancel":`Annuler`,"left.now":`maintenant`,"left.days":`dans {{days}} j {{hours}} h`,"left.hours":`dans {{hours}} h {{minutes}} min`,"left.minutes":`dans {{minutes}} min`},zh:{"pill.one":`已安排 · {{time}}`,"pill.many":`已安排:{{count}}`,"menu.title":`已安排的消息`,"row.cancel":`取消`,"left.now":`现在`,"left.days":`还有 {{days}} 天 {{hours}} 小时`,"left.hours":`还有 {{hours}} 小时 {{minutes}} 分钟`,"left.minutes":`还有 {{minutes}} 分钟`}},setup(n){let[s,l]=(0,t.createSignal)({}),u=(e,t)=>l(n=>({...n,[e]:[...(n[e]??[]).filter(e=>e.id!==t.id),t].sort((e,t)=>e.fireAt-t.fireAt)})),d=(e,t)=>l(n=>({...n,[e]:(n[e]??[]).filter(e=>e.id!==t)})),f=e=>new Date(e).toLocaleTimeString(n.locale.locale(),{hour:`2-digit`,minute:`2-digit`}),p=e=>new Date(e).toLocaleDateString(n.locale.locale(),{day:`2-digit`,month:`2-digit`}),m=e=>{let t=new Date(e),n=new Date,r=f(e);return t.toDateString()===n.toDateString()?r:`${p(e)} ${r}`},h=e=>{let t=e-Date.now();if(t<=0)return n.t(`left.now`);let r=Math.ceil(t/6e4),i=Math.floor(r/60),a=Math.floor(i/24);return a>0?n.t(`left.days`,{days:a,hours:i%24}):i>0?n.t(`left.hours`,{hours:i,minutes:r%60}):n.t(`left.minutes`,{minutes:r})},g=n.client.rpc(i);g.events.on(`scheduled`,e=>{u(e.data.sessionID,{id:e.data.id,fireAt:e.data.fireAt,preview:e.data.preview})}),g.events.on(`fired`,e=>{d(e.data.sessionID,e.data.id)}),g.events.on(`cancelled`,e=>{d(e.data.sessionID,e.data.id)});let _=async(e,t)=>{try{let{ok:n}=await g.cancel({sessionID:e,id:t});n&&d(e,t)}catch{}};(0,r.createKeyed)(()=>n.sessions.current()?.id,e=>{g.pending({sessionID:e}).then(({jobs:t})=>l(n=>({...n,[e]:t}))).catch(()=>{})}),n.add(r.Style,c),n.add(r.ComposerPill,{id:`pending`,show(e){let t=s()[e.session.id]??[];if(t.length!==0)return{label:t.length===1?n.t(`pill.one`,{time:f(t[0].fireAt)}):n.t(`pill.many`,{count:t.length}),icon:`notifications`,title:n.t(`menu.title`)}},menu:r=>(()=>{var i=a();return(0,e.insert)(i,(0,e.createComponent)(t.For,{get each(){return s()[r.session.id]??[]},children:t=>(()=>{var i=o(),a=i.firstChild,s=a.firstChild,c=s.firstChild,l=c.nextSibling,u=s.nextSibling,d=a.nextSibling;return(0,e.insert)(c,()=>m(t.fireAt)),(0,e.insert)(l,()=>h(t.fireAt)),(0,e.insert)(u,()=>t.preview),d.$$click=()=>void _(r.session.id,t.id),(0,e.effect)(t=>{var r=n.t(`row.cancel`),i=n.t(`row.cancel`);return r!==t.e&&(0,e.setAttribute)(d,`title`,t.e=r),i!==t.t&&(0,e.setAttribute)(d,`aria-label`,t.t=i),t},{e:void 0,t:void 0}),i})()})),i})()})},main(e){let t=n.Schema.Struct({id:n.Schema.String,sessionID:n.Schema.String,directory:n.Schema.String,fireAt:n.Schema.Number}),r=e.storage.store(`pending`,{schema:n.Schema.Array(t),initial:[]}),a=new Map,o=new Map,c=e=>{let t=a.get(e);t&&clearTimeout(t),a.delete(e)},l=e=>{c(e),o.delete(e),r.value.some(t=>t.id===e)&&r.set(r.value.filter(t=>t.id!==e))},u=e=>{c(e.id);let t=e.fireAt-Date.now();t>s-1?a.set(e.id,setTimeout(()=>u(e),s-1)):a.set(e.id,setTimeout(()=>void d(e),Math.max(0,t)))},d=async t=>{if(c(t.id),Date.now()<t.fireAt)u(t);else try{let{status:n}=await e.client.rpc(i).deliver({sessionID:t.sessionID,id:t.id},{location:{directory:t.directory}});if(n===`delivered`||n===`missing`){o.delete(t.id),l(t.id);return}if(n===`failed`){let e=(o.get(t.id)??0)+1;if(e>=5){o.delete(t.id),l(t.id);return}o.set(t.id,e)}a.set(t.id,setTimeout(()=>void d(t),n===`busy`?2e3:3e4))}catch{a.set(t.id,setTimeout(()=>void d(t),15e3))}},f=e=>{r.set([...r.value.filter(t=>t.id!==e.id),e]),u(e)},p=e=>{if(e.type===`rpc.delayed-message.scheduled`){let t=e.data;if(!t?.id||!t.sessionID||!t.fireAt||!t.directory)return;f({id:t.id,sessionID:t.sessionID,directory:t.directory,fireAt:t.fireAt})}else if(e.type===`rpc.delayed-message.fired`||e.type===`rpc.delayed-message.cancelled`){let t=e.data;t?.id&&l(t.id)}};for(let e of r.value)u(e);(async()=>{for(;!e.signal.aborted;){try{for await(let t of e.client.event.subscribe({signal:e.signal}))p(t)}catch{}if(e.signal.aborted)break;await new Promise(e=>setTimeout(e,3e3))}})(),e.onCleanup(()=>{for(let e of a.values())clearTimeout(e);a.clear()})}});(0,e.delegateEvents)([`click`]),exports.default=l;
1
+ Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:`Module`}});let e=require("solid-js/web"),t=require("solid-js"),n=require("effect"),r=require("@opencode/plugin/gui");var i=require("@opencode/plugin/rpc").Rpc.define({id:`delayed-message`,methods:{pending:{input:{type:`object`,properties:{sessionID:{type:`string`}},required:[`sessionID`],additionalProperties:!1},output:{type:`object`,properties:{jobs:{type:`array`,items:{type:`object`,properties:{id:{type:`string`},fireAt:{type:`number`},preview:{type:`string`}},required:[`id`,`fireAt`,`preview`],additionalProperties:!1}}},required:[`jobs`],additionalProperties:!1}},cancel:{input:{type:`object`,properties:{sessionID:{type:`string`},id:{type:`string`}},required:[`sessionID`,`id`],additionalProperties:!1},output:{type:`object`,properties:{ok:{type:`boolean`}},required:[`ok`],additionalProperties:!1}},deliver:{input:{type:`object`,properties:{sessionID:{type:`string`},id:{type:`string`}},required:[`sessionID`,`id`],additionalProperties:!1},output:{type:`object`,properties:{status:{type:`string`}},required:[`status`],additionalProperties:!1}},edit:{input:{type:`object`,properties:{sessionID:{type:`string`},id:{type:`string`}},required:[`sessionID`,`id`],additionalProperties:!1},output:{type:`object`,properties:{ok:{type:`boolean`},text:{type:`string`},fireAt:{type:`number`}},required:[`ok`],additionalProperties:!1}}},events:{scheduled:{schema:{type:`object`,properties:{sessionID:{type:`string`},id:{type:`string`},fireAt:{type:`number`},preview:{type:`string`},directory:{type:`string`}},required:[`sessionID`,`id`,`fireAt`,`preview`,`directory`],additionalProperties:!1}},fired:{schema:{type:`object`,properties:{sessionID:{type:`string`},id:{type:`string`}},required:[`sessionID`,`id`],additionalProperties:!1}},cancelled:{schema:{type:`object`,properties:{sessionID:{type:`string`},id:{type:`string`}},required:[`sessionID`,`id`],additionalProperties:!1}}}}),a=(0,e.template)(`<div class=dm>`),o=(0,e.template)(`<div class=dm-row><span class=dm-time></span><span class=dm-text></span><span class=dm-left></span><span class=dm-actions><button class=dm-action>✎</button><button class=dm-action>×`),s=2**31-1,c=e=>String(e).padStart(2,`0`),l=e=>{let t=new Date(e),n=`${t.getFullYear()}-${c(t.getMonth()+1)}-${c(t.getDate())}`,r=`${c(t.getHours())}:${c(t.getMinutes())}`;return t.getSeconds()===0?`${n}T${r}`:`${n}T${r}:${c(t.getSeconds())}`},u=`
2
+ .dm { display: flex; flex-direction: column; gap: 1px; min-width: 280px; max-width: 440px; margin: -4px -6px; }
3
+ .dm-row { position: relative; display: flex; align-items: center; gap: 8px; padding: 5px 6px; border-radius: 8px; font-size: 13px; }
4
+ .dm-row:hover { background: color-mix(in srgb, currentColor 8%, transparent); }
5
+ .dm-time { flex: none; font-weight: 500; font-variant-numeric: tabular-nums; }
6
+ .dm-text { flex: 1 1 auto; min-width: 0; opacity: .72; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
7
+ .dm-left { flex: none; min-width: 52px; text-align: right; opacity: .5; font-size: 12px; font-variant-numeric: tabular-nums; white-space: nowrap; transition: opacity .12s ease; }
8
+ .dm-actions { position: absolute; right: 6px; top: 50%; transform: translateY(-50%); display: flex; gap: 2px; opacity: 0; pointer-events: none; transition: opacity .12s ease; }
9
+ .dm-row:hover .dm-left, .dm-row:focus-within .dm-left { opacity: 0; }
10
+ .dm-row:hover .dm-actions, .dm-row:focus-within .dm-actions { opacity: 1; pointer-events: auto; }
11
+ .dm-action { flex: none; width: 24px; height: 24px; border: 0; border-radius: 6px; background: transparent; color: inherit; opacity: .55; cursor: pointer; font: inherit; line-height: 1; }
12
+ .dm-action:hover { opacity: 1; background: color-mix(in srgb, currentColor 14%, transparent); }
13
+ `,d=r.Plugin.define({i18n:{en:{"pill.one":`Scheduled · {{time}}`,"pill.many":`Scheduled: {{count}}`,"menu.title":`Scheduled messages`,"row.cancel":`Cancel`,"row.edit":`Edit`,"left.now":`now`,"left.days":`{{days}}d {{hours}}h`,"left.hours":`{{hours}}h {{minutes}}m`,"left.minutes":`{{minutes}}m`},ru:{"pill.one":`Отложенное · {{time}}`,"pill.many":`Отложенных: {{count}}`,"menu.title":`Отложенные сообщения`,"row.cancel":`Отменить`,"row.edit":`Изменить`,"left.now":`сейчас`,"left.days":`{{days}} д {{hours}} ч`,"left.hours":`{{hours}} ч {{minutes}} мин`,"left.minutes":`{{minutes}} мин`},de:{"pill.one":`Geplant · {{time}}`,"pill.many":`Geplant: {{count}}`,"menu.title":`Geplante Nachrichten`,"row.cancel":`Abbrechen`,"row.edit":`Bearbeiten`,"left.now":`jetzt`,"left.days":`{{days}} T {{hours}} Std.`,"left.hours":`{{hours}} Std. {{minutes}} Min.`,"left.minutes":`{{minutes}} Min.`},fr:{"pill.one":`Programmé · {{time}}`,"pill.many":`Programmés : {{count}}`,"menu.title":`Messages programmés`,"row.cancel":`Annuler`,"row.edit":`Modifier`,"left.now":`maintenant`,"left.days":`{{days}} j {{hours}} h`,"left.hours":`{{hours}} h {{minutes}} min`,"left.minutes":`{{minutes}} min`},zh:{"pill.one":`已安排 · {{time}}`,"pill.many":`已安排:{{count}}`,"menu.title":`已安排的消息`,"row.cancel":`取消`,"row.edit":`编辑`,"left.now":`现在`,"left.days":`{{days}} 天 {{hours}} 小时`,"left.hours":`{{hours}} 小时 {{minutes}} 分钟`,"left.minutes":`{{minutes}} 分钟`}},setup(n){let[s,c]=(0,t.createSignal)({}),d=(e,t)=>c(n=>({...n,[e]:[...(n[e]??[]).filter(e=>e.id!==t.id),t].sort((e,t)=>e.fireAt-t.fireAt)})),f=(e,t)=>c(n=>({...n,[e]:(n[e]??[]).filter(e=>e.id!==t)})),p=e=>new Date(e).toLocaleTimeString(n.locale.locale(),{hour:`2-digit`,minute:`2-digit`}),m=e=>new Date(e).toLocaleDateString(n.locale.locale(),{day:`2-digit`,month:`2-digit`}),h=e=>{let t=new Date(e),n=new Date,r=p(e);return t.toDateString()===n.toDateString()?r:`${m(e)} ${r}`},g=e=>{let t=e-Date.now();if(t<=0)return n.t(`left.now`);let r=Math.ceil(t/6e4),i=Math.floor(r/60),a=Math.floor(i/24);return a>0?n.t(`left.days`,{days:a,hours:i%24}):i>0?n.t(`left.hours`,{hours:i,minutes:r%60}):n.t(`left.minutes`,{minutes:r})},_=n.client.rpc(i);_.events.on(`scheduled`,e=>{d(e.data.sessionID,{id:e.data.id,fireAt:e.data.fireAt,preview:e.data.preview})}),_.events.on(`fired`,e=>{f(e.data.sessionID,e.data.id)}),_.events.on(`cancelled`,e=>{f(e.data.sessionID,e.data.id)});let v=async(e,t)=>{try{let{ok:n}=await _.cancel({sessionID:e,id:t});n&&f(e,t)}catch{}};(0,r.createKeyed)(()=>n.sessions.current()?.id,e=>{_.pending({sessionID:e}).then(({jobs:t})=>c(n=>({...n,[e]:t}))).catch(()=>{})}),n.add(r.Style,u),n.add(r.ComposerPill,{id:`pending`,show(e){let t=s()[e.session.id]??[];if(t.length!==0)return{label:t.length===1?n.t(`pill.one`,{time:p(t[0].fireAt)}):n.t(`pill.many`,{count:t.length}),icon:`notifications`,title:n.t(`menu.title`)}},menu:r=>(()=>{var i=a();return(0,e.insert)(i,(0,e.createComponent)(t.For,{get each(){return s()[r.session.id]??[]},children:t=>(()=>{var i=o(),a=i.firstChild,s=a.nextSibling,c=s.nextSibling,u=c.nextSibling.firstChild,d=u.nextSibling;return(0,e.insert)(a,()=>h(t.fireAt)),(0,e.insert)(s,()=>t.preview),(0,e.insert)(c,()=>g(t.fireAt)),u.$$click=()=>{_.edit({sessionID:r.session.id,id:t.id}).then(({ok:e,text:n,fireAt:i})=>{e&&n!==void 0&&i!==void 0&&(f(r.session.id,t.id),r.screen.composer.setText(`/delayed-message ${l(i)} ${n}`),r.close(),r.screen.composer.focus())}).catch(()=>{})},d.$$click=()=>void v(r.session.id,t.id),(0,e.effect)(t=>{var r=n.t(`row.edit`),i=n.t(`row.edit`),a=n.t(`row.cancel`),o=n.t(`row.cancel`);return r!==t.e&&(0,e.setAttribute)(u,`title`,t.e=r),i!==t.t&&(0,e.setAttribute)(u,`aria-label`,t.t=i),a!==t.a&&(0,e.setAttribute)(d,`title`,t.a=a),o!==t.o&&(0,e.setAttribute)(d,`aria-label`,t.o=o),t},{e:void 0,t:void 0,a:void 0,o:void 0}),i})()})),i})()})},main(e){let t=n.Schema.Struct({id:n.Schema.String,sessionID:n.Schema.String,directory:n.Schema.String,fireAt:n.Schema.Number}),r=e.storage.store(`pending`,{schema:n.Schema.Array(t),initial:[]}),a=new Map,o=new Map,c=e=>{let t=a.get(e);t&&clearTimeout(t),a.delete(e)},l=e=>{c(e),o.delete(e),r.value.some(t=>t.id===e)&&r.set(r.value.filter(t=>t.id!==e))},u=e=>{c(e.id);let t=e.fireAt-Date.now();t>s-1?a.set(e.id,setTimeout(()=>u(e),s-1)):a.set(e.id,setTimeout(()=>void d(e),Math.max(0,t)))},d=async t=>{if(c(t.id),Date.now()<t.fireAt)u(t);else try{let{status:n}=await e.client.rpc(i).deliver({sessionID:t.sessionID,id:t.id},{location:{directory:t.directory}});if(n===`delivered`||n===`missing`){o.delete(t.id),l(t.id);return}if(n===`failed`){let e=(o.get(t.id)??0)+1;if(e>=5){o.delete(t.id),l(t.id);return}o.set(t.id,e)}a.set(t.id,setTimeout(()=>void d(t),n===`busy`?2e3:3e4))}catch{a.set(t.id,setTimeout(()=>void d(t),15e3))}},f=e=>{r.set([...r.value.filter(t=>t.id!==e.id),e]),u(e)},p=e=>{if(e.type===`rpc.delayed-message.scheduled`){let t=e.data;if(!t?.id||!t.sessionID||!t.fireAt||!t.directory)return;f({id:t.id,sessionID:t.sessionID,directory:t.directory,fireAt:t.fireAt})}else if(e.type===`rpc.delayed-message.fired`||e.type===`rpc.delayed-message.cancelled`){let t=e.data;t?.id&&l(t.id)}};for(let e of r.value)u(e);(async()=>{for(;!e.signal.aborted;){try{for await(let t of e.client.event.subscribe({signal:e.signal}))p(t)}catch{}if(e.signal.aborted)break;await new Promise(e=>setTimeout(e,3e3))}})(),e.onCleanup(()=>{for(let e of a.values())clearTimeout(e);a.clear()})}});(0,e.delegateEvents)([`click`]),exports.default=d;
package/index.ts CHANGED
@@ -87,18 +87,13 @@ export default Plugin.define({
87
87
  },
88
88
  cancel: async (input) => {
89
89
  const { sessionID, id } = input as { sessionID: string; id: string }
90
- if (claimed.has(id) || delivering.has(id)) return { ok: false }
91
- const stored = (await ctx.storage.get(key(id))) as Job | undefined
92
- if (!stored || stored.sessionID !== sessionID) return { ok: false }
93
- claimed.add(id)
94
- const timer = timers.get(id)
95
- if (timer) {
96
- clearTimeout(timer)
97
- timers.delete(id)
98
- }
99
- await ctx.storage.remove(key(id))
100
- await rpc.events.emit("cancelled", { sessionID, id }).catch(() => {})
101
- return { ok: true }
90
+ return { ok: (await withdraw(sessionID, id)) !== undefined }
91
+ },
92
+ edit: async (input) => {
93
+ const { sessionID, id } = input as { sessionID: string; id: string }
94
+ const job = await withdraw(sessionID, id)
95
+ if (!job) return { ok: false }
96
+ return { ok: true, text: job.message, fireAt: job.fireAt }
102
97
  },
103
98
  deliver: async (input) => {
104
99
  const { sessionID, id } = input as { sessionID: string; id: string }
@@ -110,6 +105,21 @@ export default Plugin.define({
110
105
  },
111
106
  })
112
107
 
108
+ const withdraw = async (sessionID: string, id: string): Promise<Job | undefined> => {
109
+ if (claimed.has(id) || delivering.has(id)) return undefined
110
+ const stored = (await ctx.storage.get(key(id))) as Job | undefined
111
+ if (!stored || stored.sessionID !== sessionID) return undefined
112
+ claimed.add(id)
113
+ const timer = timers.get(id)
114
+ if (timer) {
115
+ clearTimeout(timer)
116
+ timers.delete(id)
117
+ }
118
+ await ctx.storage.remove(key(id))
119
+ await rpc.events.emit("cancelled", { sessionID, id }).catch(() => {})
120
+ return stored
121
+ }
122
+
113
123
  const deliver = async (job: Job): Promise<Delivery> => {
114
124
  if (claimed.has(job.id)) return "delivered"
115
125
  if (delivering.has(job.id)) return "busy"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tuzelko/opencode-delayed-message",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Schedule a message that wakes an OpenCode session later, without showing it to the model first.",
5
5
  "keywords": [
6
6
  "opencode",
package/rpc.ts CHANGED
@@ -70,6 +70,29 @@ export const Delayed = Rpc.define({
70
70
  additionalProperties: false,
71
71
  },
72
72
  },
73
+ // Cancels the job and hands its full text back so the pill can put the
74
+ // command into the composer for editing.
75
+ edit: {
76
+ input: {
77
+ type: "object",
78
+ properties: {
79
+ sessionID: { type: "string" },
80
+ id: { type: "string" },
81
+ },
82
+ required: ["sessionID", "id"],
83
+ additionalProperties: false,
84
+ },
85
+ output: {
86
+ type: "object",
87
+ properties: {
88
+ ok: { type: "boolean" },
89
+ text: { type: "string" },
90
+ fireAt: { type: "number" },
91
+ },
92
+ required: ["ok"],
93
+ additionalProperties: false,
94
+ },
95
+ },
73
96
  },
74
97
  events: {
75
98
  scheduled: {