@react-grab/opencode 0.0.84 → 0.0.86

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.84";
7141
+ var VERSION = "0.0.86";
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.84";
7133
+ var VERSION = "0.0.86";
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,15 +7339,21 @@ 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.84";
7346
+ var VERSION = "0.0.86";
7347
+ try {
7348
+ fetch(`https://www.react-grab.com/api/version?source=opencode&t=${Date.now()}`).catch(() => {
7349
+ });
7350
+ } catch {
7351
+ }
7346
7352
  var OPENCODE_SDK_PORT = 4096;
7347
7353
  var opencodeInstance = null;
7348
7354
  var sessionMap = /* @__PURE__ */ new Map();
7349
7355
  var abortedSessions = /* @__PURE__ */ new Set();
7350
- var lastOpenCodeSessionId;
7356
+ var lastMessageInfo;
7351
7357
  var getOpenCodeClient = async () => {
7352
7358
  if (!opencodeInstance) {
7353
7359
  await fkill(`:${OPENCODE_SDK_PORT}`, { force: true, silent: true }).catch(
@@ -7363,9 +7369,9 @@ var getOpenCodeClient = async () => {
7363
7369
  }
7364
7370
  return opencodeInstance.client;
7365
7371
  };
7366
- var executeOpenCodePrompt = async (prompt, options, onStatus, reactGrabSessionId) => {
7372
+ var executeOpenCodePrompt = async (prompt, options, onStatus, reactGrabSessionId, signal) => {
7367
7373
  const client2 = await getOpenCodeClient();
7368
- onStatus?.("Thinking...");
7374
+ onStatus?.("Thinking\u2026");
7369
7375
  let opencodeSessionId;
7370
7376
  if (reactGrabSessionId && sessionMap.has(reactGrabSessionId)) {
7371
7377
  opencodeSessionId = sessionMap.get(reactGrabSessionId);
@@ -7381,22 +7387,39 @@ var executeOpenCodePrompt = async (prompt, options, onStatus, reactGrabSessionId
7381
7387
  sessionMap.set(reactGrabSessionId, opencodeSessionId);
7382
7388
  }
7383
7389
  }
7384
- lastOpenCodeSessionId = opencodeSessionId;
7385
7390
  const modelConfig = options?.model ? {
7386
7391
  providerID: options.model.split("/")[0],
7387
7392
  modelID: options.model.split("/")[1] || options.model
7388
7393
  } : void 0;
7389
- const promptResponse = await client2.session.prompt({
7394
+ const eventStreamResult = await client2.event.subscribe();
7395
+ await client2.session.promptAsync({
7390
7396
  path: { id: opencodeSessionId },
7391
7397
  body: {
7392
7398
  ...modelConfig && { model: modelConfig },
7393
7399
  parts: [{ type: "text", text: prompt }]
7394
7400
  }
7395
7401
  });
7396
- if (promptResponse.data?.parts) {
7397
- 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
+ }
7398
7417
  if (part.type === "text" && part.text) {
7399
- 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}`);
7400
7423
  }
7401
7424
  }
7402
7425
  }
@@ -7416,7 +7439,14 @@ Context:
7416
7439
  ${content}
7417
7440
  `;
7418
7441
  return streamSSE(context, async (stream2) => {
7419
- 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
+ };
7420
7450
  try {
7421
7451
  await executeOpenCodePrompt(
7422
7452
  formattedPrompt,
@@ -7429,11 +7459,12 @@ ${content}
7429
7459
  }).catch(() => {
7430
7460
  });
7431
7461
  },
7432
- sessionId
7462
+ sessionId,
7463
+ signal
7433
7464
  );
7434
7465
  if (!isAborted()) {
7435
7466
  await stream2.writeSSE({
7436
- data: "Completed successfully",
7467
+ data: COMPLETED_STATUS,
7437
7468
  event: "status"
7438
7469
  });
7439
7470
  await stream2.writeSSE({ data: "", event: "done" });
@@ -7465,16 +7496,14 @@ ${stderr.trim()}` : errorMessage;
7465
7496
  return context.json({ status: "ok" });
7466
7497
  });
7467
7498
  honoApplication.post("/undo", async (context) => {
7468
- if (!lastOpenCodeSessionId) {
7469
- return context.json({ status: "error", message: "No session to undo" });
7499
+ if (!lastMessageInfo) {
7500
+ return context.json({ status: "error", message: "No message to undo" });
7470
7501
  }
7471
7502
  try {
7472
7503
  const client2 = await getOpenCodeClient();
7473
- await client2.session.prompt({
7474
- path: { id: lastOpenCodeSessionId },
7475
- body: {
7476
- parts: [{ type: "text", text: "/undo" }]
7477
- }
7504
+ await client2.session.revert({
7505
+ path: { id: lastMessageInfo.sessionId },
7506
+ body: { messageID: lastMessageInfo.messageId }
7478
7507
  });
7479
7508
  return context.json({ status: "ok" });
7480
7509
  } catch (error) {
@@ -7494,7 +7523,7 @@ var startServer = async (port = DEFAULT_PORT) => {
7494
7523
  const honoApplication = createServer();
7495
7524
  serve({ fetch: honoApplication.fetch, port });
7496
7525
  console.log(
7497
- `${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)")}`
7498
7527
  );
7499
7528
  console.log(`- Local: ${import_picocolors.default.cyan(`http://localhost:${port}`)}`);
7500
7529
  };
package/dist/server.js CHANGED
@@ -7328,15 +7328,21 @@ 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.84";
7335
+ var VERSION = "0.0.86";
7336
+ try {
7337
+ fetch(`https://www.react-grab.com/api/version?source=opencode&t=${Date.now()}`).catch(() => {
7338
+ });
7339
+ } catch {
7340
+ }
7335
7341
  var OPENCODE_SDK_PORT = 4096;
7336
7342
  var opencodeInstance = null;
7337
7343
  var sessionMap = /* @__PURE__ */ new Map();
7338
7344
  var abortedSessions = /* @__PURE__ */ new Set();
7339
- var lastOpenCodeSessionId;
7345
+ var lastMessageInfo;
7340
7346
  var getOpenCodeClient = async () => {
7341
7347
  if (!opencodeInstance) {
7342
7348
  await fkill(`:${OPENCODE_SDK_PORT}`, { force: true, silent: true }).catch(
@@ -7352,9 +7358,9 @@ var getOpenCodeClient = async () => {
7352
7358
  }
7353
7359
  return opencodeInstance.client;
7354
7360
  };
7355
- var executeOpenCodePrompt = async (prompt, options, onStatus, reactGrabSessionId) => {
7361
+ var executeOpenCodePrompt = async (prompt, options, onStatus, reactGrabSessionId, signal) => {
7356
7362
  const client2 = await getOpenCodeClient();
7357
- onStatus?.("Thinking...");
7363
+ onStatus?.("Thinking\u2026");
7358
7364
  let opencodeSessionId;
7359
7365
  if (reactGrabSessionId && sessionMap.has(reactGrabSessionId)) {
7360
7366
  opencodeSessionId = sessionMap.get(reactGrabSessionId);
@@ -7370,22 +7376,39 @@ var executeOpenCodePrompt = async (prompt, options, onStatus, reactGrabSessionId
7370
7376
  sessionMap.set(reactGrabSessionId, opencodeSessionId);
7371
7377
  }
7372
7378
  }
7373
- lastOpenCodeSessionId = opencodeSessionId;
7374
7379
  const modelConfig = options?.model ? {
7375
7380
  providerID: options.model.split("/")[0],
7376
7381
  modelID: options.model.split("/")[1] || options.model
7377
7382
  } : void 0;
7378
- const promptResponse = await client2.session.prompt({
7383
+ const eventStreamResult = await client2.event.subscribe();
7384
+ await client2.session.promptAsync({
7379
7385
  path: { id: opencodeSessionId },
7380
7386
  body: {
7381
7387
  ...modelConfig && { model: modelConfig },
7382
7388
  parts: [{ type: "text", text: prompt }]
7383
7389
  }
7384
7390
  });
7385
- if (promptResponse.data?.parts) {
7386
- 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
+ }
7387
7406
  if (part.type === "text" && part.text) {
7388
- 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}`);
7389
7412
  }
7390
7413
  }
7391
7414
  }
@@ -7405,7 +7428,14 @@ Context:
7405
7428
  ${content}
7406
7429
  `;
7407
7430
  return streamSSE(context, async (stream2) => {
7408
- 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
+ };
7409
7439
  try {
7410
7440
  await executeOpenCodePrompt(
7411
7441
  formattedPrompt,
@@ -7418,11 +7448,12 @@ ${content}
7418
7448
  }).catch(() => {
7419
7449
  });
7420
7450
  },
7421
- sessionId
7451
+ sessionId,
7452
+ signal
7422
7453
  );
7423
7454
  if (!isAborted()) {
7424
7455
  await stream2.writeSSE({
7425
- data: "Completed successfully",
7456
+ data: COMPLETED_STATUS,
7426
7457
  event: "status"
7427
7458
  });
7428
7459
  await stream2.writeSSE({ data: "", event: "done" });
@@ -7454,16 +7485,14 @@ ${stderr.trim()}` : errorMessage;
7454
7485
  return context.json({ status: "ok" });
7455
7486
  });
7456
7487
  honoApplication.post("/undo", async (context) => {
7457
- if (!lastOpenCodeSessionId) {
7458
- return context.json({ status: "error", message: "No session to undo" });
7488
+ if (!lastMessageInfo) {
7489
+ return context.json({ status: "error", message: "No message to undo" });
7459
7490
  }
7460
7491
  try {
7461
7492
  const client2 = await getOpenCodeClient();
7462
- await client2.session.prompt({
7463
- path: { id: lastOpenCodeSessionId },
7464
- body: {
7465
- parts: [{ type: "text", text: "/undo" }]
7466
- }
7493
+ await client2.session.revert({
7494
+ path: { id: lastMessageInfo.sessionId },
7495
+ body: { messageID: lastMessageInfo.messageId }
7467
7496
  });
7468
7497
  return context.json({ status: "ok" });
7469
7498
  } catch (error) {
@@ -7483,7 +7512,7 @@ var startServer = async (port = DEFAULT_PORT) => {
7483
7512
  const honoApplication = createServer();
7484
7513
  serve({ fetch: honoApplication.fetch, port });
7485
7514
  console.log(
7486
- `${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)")}`
7487
7516
  );
7488
7517
  console.log(`- Local: ${import_picocolors.default.cyan(`http://localhost:${port}`)}`);
7489
7518
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@react-grab/opencode",
3
- "version": "0.0.84",
3
+ "version": "0.0.86",
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.84"
29
+ "@react-grab/utils": "0.0.86"
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.84"
38
+ "react-grab": "0.0.86"
38
39
  },
39
40
  "scripts": {
40
41
  "dev": "tsup --watch",