@react-grab/opencode 0.0.85 → 0.0.87

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/dist/cli.cjs CHANGED
@@ -7138,7 +7138,7 @@ var import_picocolors = __toESM(require_picocolors());
7138
7138
  var DEFAULT_PORT = 6567;
7139
7139
 
7140
7140
  // src/cli.ts
7141
- var VERSION = "0.0.85";
7141
+ var VERSION = "0.0.87";
7142
7142
  var serverPath = path2.join(__dirname, "server.cjs");
7143
7143
  execa(process.execPath, [serverPath], {
7144
7144
  detached: true,
@@ -7146,6 +7146,6 @@ execa(process.execPath, [serverPath], {
7146
7146
  cleanup: false
7147
7147
  }).unref();
7148
7148
  console.log(
7149
- `${import_picocolors.default.magenta("\u269B")} ${import_picocolors.default.bold("React Grab")} ${import_picocolors.default.gray(VERSION)} ${import_picocolors.default.dim("(OpenCode)")}`
7149
+ `${import_picocolors.default.magenta("\u273F")} ${import_picocolors.default.bold("React Grab")} ${import_picocolors.default.gray(VERSION)} ${import_picocolors.default.dim("(OpenCode)")}`
7150
7150
  );
7151
7151
  console.log(`- Local: ${import_picocolors.default.cyan(`http://localhost:${DEFAULT_PORT}`)}`);
package/dist/cli.js CHANGED
@@ -7130,7 +7130,7 @@ var import_picocolors = __toESM(require_picocolors());
7130
7130
  var DEFAULT_PORT = 6567;
7131
7131
 
7132
7132
  // src/cli.ts
7133
- var VERSION = "0.0.85";
7133
+ var VERSION = "0.0.87";
7134
7134
  var serverPath = join(__dirname, "server.cjs");
7135
7135
  execa(process.execPath, [serverPath], {
7136
7136
  detached: true,
@@ -7138,6 +7138,6 @@ execa(process.execPath, [serverPath], {
7138
7138
  cleanup: false
7139
7139
  }).unref();
7140
7140
  console.log(
7141
- `${import_picocolors.default.magenta("\u269B")} ${import_picocolors.default.bold("React Grab")} ${import_picocolors.default.gray(VERSION)} ${import_picocolors.default.dim("(OpenCode)")}`
7141
+ `${import_picocolors.default.magenta("\u273F")} ${import_picocolors.default.bold("React Grab")} ${import_picocolors.default.gray(VERSION)} ${import_picocolors.default.dim("(OpenCode)")}`
7142
7142
  );
7143
7143
  console.log(`- Local: ${import_picocolors.default.cyan(`http://localhost:${DEFAULT_PORT}`)}`);
package/dist/client.cjs CHANGED
@@ -55,6 +55,7 @@ async function* streamSSE(stream, signal) {
55
55
 
56
56
  // src/constants.ts
57
57
  var DEFAULT_PORT = 6567;
58
+ var COMPLETED_STATUS = "Completed successfully";
58
59
 
59
60
  // src/client.ts
60
61
  var DEFAULT_SERVER_URL = `http://localhost:${DEFAULT_PORT}`;
@@ -86,6 +87,7 @@ var streamFromServer = async function* (serverUrl, context, signal) {
86
87
  const iterator = streamSSE(response.body, signal)[Symbol.asyncIterator]();
87
88
  let done = false;
88
89
  let pendingNext = iterator.next();
90
+ let lastStatus = null;
89
91
  while (!done) {
90
92
  const result = await Promise.race([
91
93
  pendingNext.then((iteratorResult) => ({
@@ -96,23 +98,22 @@ var streamFromServer = async function* (serverUrl, context, signal) {
96
98
  (resolve) => setTimeout(() => resolve({ type: "timeout" }), 100)
97
99
  )
98
100
  ]);
99
- if (result.type === "timeout") {
100
- const elapsedSeconds = (Date.now() - startTime) / 1e3;
101
- yield `Working\u2026 ${elapsedSeconds.toFixed(1)}s`;
102
- } else {
101
+ const elapsedSeconds = (Date.now() - startTime) / 1e3;
102
+ if (result.type === "status") {
103
103
  const iteratorResult = result.iteratorResult;
104
104
  done = iteratorResult.done ?? false;
105
105
  if (!done && iteratorResult.value) {
106
- const status = iteratorResult.value;
107
- if (status === "Completed successfully") {
108
- const totalSeconds = ((Date.now() - startTime) / 1e3).toFixed(1);
109
- yield `Completed in ${totalSeconds}s`;
110
- } else {
111
- yield status;
112
- }
106
+ lastStatus = iteratorResult.value;
113
107
  pendingNext = iterator.next();
114
108
  }
115
109
  }
110
+ if (lastStatus === COMPLETED_STATUS) {
111
+ yield `Completed in ${elapsedSeconds.toFixed(1)}s`;
112
+ } else if (lastStatus) {
113
+ yield `${lastStatus} ${elapsedSeconds.toFixed(1)}s`;
114
+ } else {
115
+ yield `Working\u2026 ${elapsedSeconds.toFixed(1)}s`;
116
+ }
116
117
  }
117
118
  } finally {
118
119
  signal.removeEventListener("abort", handleAbort);
@@ -1,5 +1,5 @@
1
- var ReactGrabOpenCode=(function(exports){'use strict';var f=5e3,m="react-grab:agent-sessions",y=c=>{let e="",n="";for(let t of c.split(`
2
- `))t.startsWith("event:")?e=t.slice(6).trim():t.startsWith("data:")&&(n=t.slice(5).trim());return {eventType:e,data:n}};async function*g(c,e){let n=c.getReader(),t=new TextDecoder,r="",o=false,i=()=>{o=true,n.cancel().catch(()=>{});};e.addEventListener("abort",i);try{if(e.aborted)throw new DOMException("Aborted","AbortError");for(;;){let a=await n.read();if(o||e.aborted)throw new DOMException("Aborted","AbortError");let{done:p,value:u}=a;u&&(r+=t.decode(u,{stream:!0}));let d;for(;(d=r.indexOf(`
1
+ var ReactGrabOpenCode=(function(exports){'use strict';var g=5e3,m="react-grab:agent-sessions",C=c=>{let e="",o="";for(let n of c.split(`
2
+ `))n.startsWith("event:")?e=n.slice(6).trim():n.startsWith("data:")&&(o=n.slice(5).trim());return {eventType:e,data:o}};async function*A(c,e){let o=c.getReader(),n=new TextDecoder,r="",t=false,s=()=>{t=true,o.cancel().catch(()=>{});};e.addEventListener("abort",s);try{if(e.aborted)throw new DOMException("Aborted","AbortError");for(;;){let i=await o.read();if(t||e.aborted)throw new DOMException("Aborted","AbortError");let{done:l,value:f}=i;f&&(r+=n.decode(f,{stream:!0}));let a;for(;(a=r.indexOf(`
3
3
 
4
- `))!==-1;){let{eventType:s,data:l}=y(r.slice(0,d));if(r=r.slice(d+2),s==="done")return;if(s==="error")throw new Error(l||"Agent error");l&&(yield l);}if(p)break}}finally{e.removeEventListener("abort",i);try{n.releaseLock();}catch{}}}var C=`http://localhost:${6567}`,A=async function*(c,e,n){let t=Date.now(),r=e.sessionId,o=()=>{r&&fetch(`${c}/abort/${r}`,{method:"POST"}).catch(()=>{});};n.addEventListener("abort",o);try{let i=await fetch(`${c}/agent`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e),signal:n});if(!i.ok)throw new Error(`Server error: ${i.status}`);if(!i.body)throw new Error("No response body");let a=g(i.body,n)[Symbol.asyncIterator](),p=!1,u=a.next();for(;!p;){let d=await Promise.race([u.then(s=>({type:"status",iteratorResult:s})),new Promise(s=>setTimeout(()=>s({type:"timeout"}),100))]);if(d.type==="timeout")yield `Working\u2026 ${((Date.now()-t)/1e3).toFixed(1)}s`;else {let s=d.iteratorResult;if(p=s.done??!1,!p&&s.value){let l=s.value;l==="Completed successfully"?yield `Completed in ${((Date.now()-t)/1e3).toFixed(1)}s`:yield l,u=a.next();}}}}finally{n.removeEventListener("abort",o);}},E=(c={})=>{let{serverUrl:e=C,getOptions:n}=c,t=null,r=o=>({...n?.()??{},...o??{}});return {send:async function*(o,i){let a={...o,options:r(o.options)};yield*A(e,a,i);},resume:async function*(o,i,a){let p=a.getItem(m);if(!p)throw new Error("No sessions to resume");let d=JSON.parse(p)[o];if(!d)throw new Error(`Session ${o} not found`);let s=d.context,l={...s,options:r(s.options)};yield "Resuming...",yield*A(e,l,i);},supportsResume:true,supportsFollowUp:true,checkConnection:async()=>{let o=Date.now();if(t&&o-t.timestamp<f)return t.result;try{let a=(await fetch(`${e}/health`,{method:"GET"})).ok;return t={result:a,timestamp:o},a}catch{return t={result:false,timestamp:o},false}},undo:async()=>{try{await fetch(`${e}/undo`,{method:"POST"});}catch{}}}},O=async()=>{if(typeof window>"u")return;let c=E(),e=r=>{r.setAgent({provider:c,storage:sessionStorage});},n=window.__REACT_GRAB__;if(n){e(n);return}window.addEventListener("react-grab:init",r=>{e(r.detail);},{once:true});let t=window.__REACT_GRAB__;t&&e(t);};O();
5
- exports.attachAgent=O;exports.createOpenCodeAgentProvider=E;return exports;})({});
4
+ `))!==-1;){let{eventType:p,data:d}=C(r.slice(0,a));if(r=r.slice(a+2),p==="done")return;if(p==="error")throw new Error(d||"Agent error");d&&(yield d);}if(l)break}}finally{e.removeEventListener("abort",s);try{o.releaseLock();}catch{}}}var y="Completed successfully";var w=`http://localhost:${6567}`,E=async function*(c,e,o){let n=Date.now(),r=e.sessionId,t=()=>{r&&fetch(`${c}/abort/${r}`,{method:"POST"}).catch(()=>{});};o.addEventListener("abort",t);try{let s=await fetch(`${c}/agent`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e),signal:o});if(!s.ok)throw new Error(`Server error: ${s.status}`);if(!s.body)throw new Error("No response body");let i=A(s.body,o)[Symbol.asyncIterator](),l=!1,f=i.next(),a=null;for(;!l;){let p=await Promise.race([f.then(u=>({type:"status",iteratorResult:u})),new Promise(u=>setTimeout(()=>u({type:"timeout"}),100))]),d=(Date.now()-n)/1e3;if(p.type==="status"){let u=p.iteratorResult;l=u.done??!1,!l&&u.value&&(a=u.value,f=i.next());}a===y?yield `Completed in ${d.toFixed(1)}s`:a?yield `${a} ${d.toFixed(1)}s`:yield `Working\u2026 ${d.toFixed(1)}s`;}}finally{o.removeEventListener("abort",t);}},S=(c={})=>{let{serverUrl:e=w,getOptions:o}=c,n=null,r=t=>({...o?.()??{},...t??{}});return {send:async function*(t,s){let i={...t,options:r(t.options)};yield*E(e,i,s);},resume:async function*(t,s,i){let l=i.getItem(m);if(!l)throw new Error("No sessions to resume");let a=JSON.parse(l)[t];if(!a)throw new Error(`Session ${t} not found`);let p=a.context,d={...p,options:r(p.options)};yield "Resuming...",yield*E(e,d,s);},supportsResume:true,supportsFollowUp:true,checkConnection:async()=>{let t=Date.now();if(n&&t-n.timestamp<g)return n.result;try{let i=(await fetch(`${e}/health`,{method:"GET"})).ok;return n={result:i,timestamp:t},i}catch{return n={result:false,timestamp:t},false}},undo:async()=>{try{await fetch(`${e}/undo`,{method:"POST"});}catch{}}}},b=async()=>{if(typeof window>"u")return;let c=S(),e=r=>{r.setAgent({provider:c,storage:sessionStorage});},o=window.__REACT_GRAB__;if(o){e(o);return}window.addEventListener("react-grab:init",r=>{e(r.detail);},{once:true});let n=window.__REACT_GRAB__;n&&e(n);};b();
5
+ exports.attachAgent=b;exports.createOpenCodeAgentProvider=S;return exports;})({});
package/dist/client.js CHANGED
@@ -53,6 +53,7 @@ async function* streamSSE(stream, signal) {
53
53
 
54
54
  // src/constants.ts
55
55
  var DEFAULT_PORT = 6567;
56
+ var COMPLETED_STATUS = "Completed successfully";
56
57
 
57
58
  // src/client.ts
58
59
  var DEFAULT_SERVER_URL = `http://localhost:${DEFAULT_PORT}`;
@@ -84,6 +85,7 @@ var streamFromServer = async function* (serverUrl, context, signal) {
84
85
  const iterator = streamSSE(response.body, signal)[Symbol.asyncIterator]();
85
86
  let done = false;
86
87
  let pendingNext = iterator.next();
88
+ let lastStatus = null;
87
89
  while (!done) {
88
90
  const result = await Promise.race([
89
91
  pendingNext.then((iteratorResult) => ({
@@ -94,23 +96,22 @@ var streamFromServer = async function* (serverUrl, context, signal) {
94
96
  (resolve) => setTimeout(() => resolve({ type: "timeout" }), 100)
95
97
  )
96
98
  ]);
97
- if (result.type === "timeout") {
98
- const elapsedSeconds = (Date.now() - startTime) / 1e3;
99
- yield `Working\u2026 ${elapsedSeconds.toFixed(1)}s`;
100
- } else {
99
+ const elapsedSeconds = (Date.now() - startTime) / 1e3;
100
+ if (result.type === "status") {
101
101
  const iteratorResult = result.iteratorResult;
102
102
  done = iteratorResult.done ?? false;
103
103
  if (!done && iteratorResult.value) {
104
- const status = iteratorResult.value;
105
- if (status === "Completed successfully") {
106
- const totalSeconds = ((Date.now() - startTime) / 1e3).toFixed(1);
107
- yield `Completed in ${totalSeconds}s`;
108
- } else {
109
- yield status;
110
- }
104
+ lastStatus = iteratorResult.value;
111
105
  pendingNext = iterator.next();
112
106
  }
113
107
  }
108
+ if (lastStatus === COMPLETED_STATUS) {
109
+ yield `Completed in ${elapsedSeconds.toFixed(1)}s`;
110
+ } else if (lastStatus) {
111
+ yield `${lastStatus} ${elapsedSeconds.toFixed(1)}s`;
112
+ } else {
113
+ yield `Working\u2026 ${elapsedSeconds.toFixed(1)}s`;
114
+ }
114
115
  }
115
116
  } finally {
116
117
  signal.removeEventListener("abort", handleAbort);
package/dist/server.cjs CHANGED
@@ -7339,10 +7339,11 @@ var import_picocolors = __toESM(require_picocolors());
7339
7339
 
7340
7340
  // src/constants.ts
7341
7341
  var DEFAULT_PORT = 6567;
7342
+ var COMPLETED_STATUS = "Completed successfully";
7342
7343
 
7343
7344
  // ../utils/dist/server.js
7344
7345
  var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
7345
- var VERSION = "0.0.85";
7346
+ var VERSION = "0.0.87";
7346
7347
  try {
7347
7348
  fetch(`https://www.react-grab.com/api/version?source=opencode&t=${Date.now()}`).catch(() => {
7348
7349
  });
@@ -7352,7 +7353,7 @@ var OPENCODE_SDK_PORT = 4096;
7352
7353
  var opencodeInstance = null;
7353
7354
  var sessionMap = /* @__PURE__ */ new Map();
7354
7355
  var abortedSessions = /* @__PURE__ */ new Set();
7355
- var lastOpenCodeSessionId;
7356
+ var lastMessageInfo;
7356
7357
  var getOpenCodeClient = async () => {
7357
7358
  if (!opencodeInstance) {
7358
7359
  await fkill(`:${OPENCODE_SDK_PORT}`, { force: true, silent: true }).catch(
@@ -7368,9 +7369,9 @@ var getOpenCodeClient = async () => {
7368
7369
  }
7369
7370
  return opencodeInstance.client;
7370
7371
  };
7371
- var executeOpenCodePrompt = async (prompt, options, onStatus, reactGrabSessionId) => {
7372
+ var executeOpenCodePrompt = async (prompt, options, onStatus, reactGrabSessionId, signal) => {
7372
7373
  const client2 = await getOpenCodeClient();
7373
- onStatus?.("Thinking...");
7374
+ onStatus?.("Thinking\u2026");
7374
7375
  let opencodeSessionId;
7375
7376
  if (reactGrabSessionId && sessionMap.has(reactGrabSessionId)) {
7376
7377
  opencodeSessionId = sessionMap.get(reactGrabSessionId);
@@ -7386,22 +7387,39 @@ var executeOpenCodePrompt = async (prompt, options, onStatus, reactGrabSessionId
7386
7387
  sessionMap.set(reactGrabSessionId, opencodeSessionId);
7387
7388
  }
7388
7389
  }
7389
- lastOpenCodeSessionId = opencodeSessionId;
7390
7390
  const modelConfig = options?.model ? {
7391
7391
  providerID: options.model.split("/")[0],
7392
7392
  modelID: options.model.split("/")[1] || options.model
7393
7393
  } : void 0;
7394
- const promptResponse = await client2.session.prompt({
7394
+ const eventStreamResult = await client2.event.subscribe();
7395
+ await client2.session.promptAsync({
7395
7396
  path: { id: opencodeSessionId },
7396
7397
  body: {
7397
7398
  ...modelConfig && { model: modelConfig },
7398
7399
  parts: [{ type: "text", text: prompt }]
7399
7400
  }
7400
7401
  });
7401
- if (promptResponse.data?.parts) {
7402
- for (const part of promptResponse.data.parts) {
7402
+ for await (const event of eventStreamResult.stream) {
7403
+ if (signal?.aborted) break;
7404
+ const eventData = event;
7405
+ if (eventData.type === "session.idle") {
7406
+ const idleSessionId = eventData.properties?.sessionID;
7407
+ if (idleSessionId === opencodeSessionId) {
7408
+ break;
7409
+ }
7410
+ }
7411
+ if (eventData.type === "message.part.updated" && eventData.properties?.part) {
7412
+ const part = eventData.properties.part;
7413
+ if (part.sessionID !== opencodeSessionId) continue;
7414
+ if (part.messageID) {
7415
+ lastMessageInfo = { sessionId: opencodeSessionId, messageId: part.messageID };
7416
+ }
7403
7417
  if (part.type === "text" && part.text) {
7404
- onStatus?.(part.text);
7418
+ const truncatedText = part.text.length > 100 ? `${part.text.slice(0, 100)}...` : part.text;
7419
+ onStatus?.(truncatedText);
7420
+ } else if (part.type === "tool-invocation" && part.toolName) {
7421
+ const stateLabel = part.state === "running" ? "Running" : "Using";
7422
+ onStatus?.(`${stateLabel} ${part.toolName}`);
7405
7423
  }
7406
7424
  }
7407
7425
  }
@@ -7421,7 +7439,14 @@ Context:
7421
7439
  ${content}
7422
7440
  `;
7423
7441
  return streamSSE(context, async (stream2) => {
7424
- const isAborted = () => sessionId && abortedSessions.has(sessionId);
7442
+ const signal = { aborted: false };
7443
+ const isAborted = () => {
7444
+ if (sessionId && abortedSessions.has(sessionId)) {
7445
+ signal.aborted = true;
7446
+ return true;
7447
+ }
7448
+ return false;
7449
+ };
7425
7450
  try {
7426
7451
  await executeOpenCodePrompt(
7427
7452
  formattedPrompt,
@@ -7434,11 +7459,12 @@ ${content}
7434
7459
  }).catch(() => {
7435
7460
  });
7436
7461
  },
7437
- sessionId
7462
+ sessionId,
7463
+ signal
7438
7464
  );
7439
7465
  if (!isAborted()) {
7440
7466
  await stream2.writeSSE({
7441
- data: "Completed successfully",
7467
+ data: COMPLETED_STATUS,
7442
7468
  event: "status"
7443
7469
  });
7444
7470
  await stream2.writeSSE({ data: "", event: "done" });
@@ -7470,16 +7496,14 @@ ${stderr.trim()}` : errorMessage;
7470
7496
  return context.json({ status: "ok" });
7471
7497
  });
7472
7498
  honoApplication.post("/undo", async (context) => {
7473
- if (!lastOpenCodeSessionId) {
7474
- return context.json({ status: "error", message: "No session to undo" });
7499
+ if (!lastMessageInfo) {
7500
+ return context.json({ status: "error", message: "No message to undo" });
7475
7501
  }
7476
7502
  try {
7477
7503
  const client2 = await getOpenCodeClient();
7478
- await client2.session.prompt({
7479
- path: { id: lastOpenCodeSessionId },
7480
- body: {
7481
- parts: [{ type: "text", text: "/undo" }]
7482
- }
7504
+ await client2.session.revert({
7505
+ path: { id: lastMessageInfo.sessionId },
7506
+ body: { messageID: lastMessageInfo.messageId }
7483
7507
  });
7484
7508
  return context.json({ status: "ok" });
7485
7509
  } catch (error) {
@@ -7499,7 +7523,7 @@ var startServer = async (port = DEFAULT_PORT) => {
7499
7523
  const honoApplication = createServer();
7500
7524
  serve({ fetch: honoApplication.fetch, port });
7501
7525
  console.log(
7502
- `${import_picocolors.default.magenta("\u269B")} ${import_picocolors.default.bold("React Grab")} ${import_picocolors.default.gray(VERSION)} ${import_picocolors.default.dim("(OpenCode)")}`
7526
+ `${import_picocolors.default.magenta("\u273F")} ${import_picocolors.default.bold("React Grab")} ${import_picocolors.default.gray(VERSION)} ${import_picocolors.default.dim("(OpenCode)")}`
7503
7527
  );
7504
7528
  console.log(`- Local: ${import_picocolors.default.cyan(`http://localhost:${port}`)}`);
7505
7529
  };
package/dist/server.js CHANGED
@@ -7328,10 +7328,11 @@ var import_picocolors = __toESM(require_picocolors());
7328
7328
 
7329
7329
  // src/constants.ts
7330
7330
  var DEFAULT_PORT = 6567;
7331
+ var COMPLETED_STATUS = "Completed successfully";
7331
7332
 
7332
7333
  // ../utils/dist/server.js
7333
7334
  var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
7334
- var VERSION = "0.0.85";
7335
+ var VERSION = "0.0.87";
7335
7336
  try {
7336
7337
  fetch(`https://www.react-grab.com/api/version?source=opencode&t=${Date.now()}`).catch(() => {
7337
7338
  });
@@ -7341,7 +7342,7 @@ var OPENCODE_SDK_PORT = 4096;
7341
7342
  var opencodeInstance = null;
7342
7343
  var sessionMap = /* @__PURE__ */ new Map();
7343
7344
  var abortedSessions = /* @__PURE__ */ new Set();
7344
- var lastOpenCodeSessionId;
7345
+ var lastMessageInfo;
7345
7346
  var getOpenCodeClient = async () => {
7346
7347
  if (!opencodeInstance) {
7347
7348
  await fkill(`:${OPENCODE_SDK_PORT}`, { force: true, silent: true }).catch(
@@ -7357,9 +7358,9 @@ var getOpenCodeClient = async () => {
7357
7358
  }
7358
7359
  return opencodeInstance.client;
7359
7360
  };
7360
- var executeOpenCodePrompt = async (prompt, options, onStatus, reactGrabSessionId) => {
7361
+ var executeOpenCodePrompt = async (prompt, options, onStatus, reactGrabSessionId, signal) => {
7361
7362
  const client2 = await getOpenCodeClient();
7362
- onStatus?.("Thinking...");
7363
+ onStatus?.("Thinking\u2026");
7363
7364
  let opencodeSessionId;
7364
7365
  if (reactGrabSessionId && sessionMap.has(reactGrabSessionId)) {
7365
7366
  opencodeSessionId = sessionMap.get(reactGrabSessionId);
@@ -7375,22 +7376,39 @@ var executeOpenCodePrompt = async (prompt, options, onStatus, reactGrabSessionId
7375
7376
  sessionMap.set(reactGrabSessionId, opencodeSessionId);
7376
7377
  }
7377
7378
  }
7378
- lastOpenCodeSessionId = opencodeSessionId;
7379
7379
  const modelConfig = options?.model ? {
7380
7380
  providerID: options.model.split("/")[0],
7381
7381
  modelID: options.model.split("/")[1] || options.model
7382
7382
  } : void 0;
7383
- const promptResponse = await client2.session.prompt({
7383
+ const eventStreamResult = await client2.event.subscribe();
7384
+ await client2.session.promptAsync({
7384
7385
  path: { id: opencodeSessionId },
7385
7386
  body: {
7386
7387
  ...modelConfig && { model: modelConfig },
7387
7388
  parts: [{ type: "text", text: prompt }]
7388
7389
  }
7389
7390
  });
7390
- if (promptResponse.data?.parts) {
7391
- for (const part of promptResponse.data.parts) {
7391
+ for await (const event of eventStreamResult.stream) {
7392
+ if (signal?.aborted) break;
7393
+ const eventData = event;
7394
+ if (eventData.type === "session.idle") {
7395
+ const idleSessionId = eventData.properties?.sessionID;
7396
+ if (idleSessionId === opencodeSessionId) {
7397
+ break;
7398
+ }
7399
+ }
7400
+ if (eventData.type === "message.part.updated" && eventData.properties?.part) {
7401
+ const part = eventData.properties.part;
7402
+ if (part.sessionID !== opencodeSessionId) continue;
7403
+ if (part.messageID) {
7404
+ lastMessageInfo = { sessionId: opencodeSessionId, messageId: part.messageID };
7405
+ }
7392
7406
  if (part.type === "text" && part.text) {
7393
- onStatus?.(part.text);
7407
+ const truncatedText = part.text.length > 100 ? `${part.text.slice(0, 100)}...` : part.text;
7408
+ onStatus?.(truncatedText);
7409
+ } else if (part.type === "tool-invocation" && part.toolName) {
7410
+ const stateLabel = part.state === "running" ? "Running" : "Using";
7411
+ onStatus?.(`${stateLabel} ${part.toolName}`);
7394
7412
  }
7395
7413
  }
7396
7414
  }
@@ -7410,7 +7428,14 @@ Context:
7410
7428
  ${content}
7411
7429
  `;
7412
7430
  return streamSSE(context, async (stream2) => {
7413
- const isAborted = () => sessionId && abortedSessions.has(sessionId);
7431
+ const signal = { aborted: false };
7432
+ const isAborted = () => {
7433
+ if (sessionId && abortedSessions.has(sessionId)) {
7434
+ signal.aborted = true;
7435
+ return true;
7436
+ }
7437
+ return false;
7438
+ };
7414
7439
  try {
7415
7440
  await executeOpenCodePrompt(
7416
7441
  formattedPrompt,
@@ -7423,11 +7448,12 @@ ${content}
7423
7448
  }).catch(() => {
7424
7449
  });
7425
7450
  },
7426
- sessionId
7451
+ sessionId,
7452
+ signal
7427
7453
  );
7428
7454
  if (!isAborted()) {
7429
7455
  await stream2.writeSSE({
7430
- data: "Completed successfully",
7456
+ data: COMPLETED_STATUS,
7431
7457
  event: "status"
7432
7458
  });
7433
7459
  await stream2.writeSSE({ data: "", event: "done" });
@@ -7459,16 +7485,14 @@ ${stderr.trim()}` : errorMessage;
7459
7485
  return context.json({ status: "ok" });
7460
7486
  });
7461
7487
  honoApplication.post("/undo", async (context) => {
7462
- if (!lastOpenCodeSessionId) {
7463
- return context.json({ status: "error", message: "No session to undo" });
7488
+ if (!lastMessageInfo) {
7489
+ return context.json({ status: "error", message: "No message to undo" });
7464
7490
  }
7465
7491
  try {
7466
7492
  const client2 = await getOpenCodeClient();
7467
- await client2.session.prompt({
7468
- path: { id: lastOpenCodeSessionId },
7469
- body: {
7470
- parts: [{ type: "text", text: "/undo" }]
7471
- }
7493
+ await client2.session.revert({
7494
+ path: { id: lastMessageInfo.sessionId },
7495
+ body: { messageID: lastMessageInfo.messageId }
7472
7496
  });
7473
7497
  return context.json({ status: "ok" });
7474
7498
  } catch (error) {
@@ -7488,7 +7512,7 @@ var startServer = async (port = DEFAULT_PORT) => {
7488
7512
  const honoApplication = createServer();
7489
7513
  serve({ fetch: honoApplication.fetch, port });
7490
7514
  console.log(
7491
- `${import_picocolors.default.magenta("\u269B")} ${import_picocolors.default.bold("React Grab")} ${import_picocolors.default.gray(VERSION)} ${import_picocolors.default.dim("(OpenCode)")}`
7515
+ `${import_picocolors.default.magenta("\u273F")} ${import_picocolors.default.bold("React Grab")} ${import_picocolors.default.gray(VERSION)} ${import_picocolors.default.dim("(OpenCode)")}`
7492
7516
  );
7493
7517
  console.log(`- Local: ${import_picocolors.default.cyan(`http://localhost:${port}`)}`);
7494
7518
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@react-grab/opencode",
3
- "version": "0.0.85",
3
+ "version": "0.0.87",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "react-grab-opencode": "./dist/cli.cjs"
@@ -24,8 +24,9 @@
24
24
  "dist"
25
25
  ],
26
26
  "devDependencies": {
27
+ "@types/node": "^22.10.7",
27
28
  "tsup": "^8.4.0",
28
- "@react-grab/utils": "0.0.85"
29
+ "@react-grab/utils": "0.0.87"
29
30
  },
30
31
  "dependencies": {
31
32
  "@hono/node-server": "^1.19.6",
@@ -34,7 +35,7 @@
34
35
  "fkill": "^9.0.0",
35
36
  "hono": "^4.0.0",
36
37
  "picocolors": "^1.1.1",
37
- "react-grab": "0.0.85"
38
+ "react-grab": "0.0.87"
38
39
  },
39
40
  "scripts": {
40
41
  "dev": "tsup --watch",