@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 +10 -5
- package/gui-experimental.cjs +13 -11
- package/index.ts +22 -12
- package/package.json +1 -1
- package/rpc.ts +23 -0
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:
|
|
89
|
-
|
|
90
|
-
|
|
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
|
|
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.
|
package/gui-experimental.cjs
CHANGED
|
@@ -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><
|
|
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
|
|
4
|
-
.dm-row
|
|
5
|
-
.dm-
|
|
6
|
-
.dm-
|
|
7
|
-
.dm-left { opacity: .
|
|
8
|
-
.dm-
|
|
9
|
-
.dm-
|
|
10
|
-
.dm-
|
|
11
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
if (
|
|
96
|
-
|
|
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
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: {
|