@covro/covro 0.1.1-dev.2 → 0.1.1-dev.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
@@ -32,8 +32,10 @@
32
32
  - 📱 **Step away from your desk. Stay with the task.** Follow tool activity, inspect file changes, handle permission requests, and answer Codex's questions from your computer or phone. Closing the page doesn't cancel the task.
33
33
  - 🗂️ **See every project in one place.** Run independent tasks in parallel and spot which conversations are working, waiting for approval, or ready to read.
34
34
  - 💬 **Pick up your Codex CLI conversations.** Browse saved history, resume existing work, or fork a new direction. Rename conversations to find them later and archive them when you're done.
35
+ - ⚡ **Open long conversations sooner.** Recent messages appear first; scrolling preloads older history. On this device, cached pages can appear while the server checks for updates. [Loading and cache behavior](docs/session-history.md).
35
36
  - 📖 **Review the work beside the conversation.** Read Markdown documents with code, tables, and images, or open JSON files in a desktop side panel while you keep talking to Codex. JSON previews include indentation, syntax highlighting, and original text for invalid JSON. Copy the unchanged source, with a full-screen reader on mobile. File previews are read-only; Markdown and JSON files have a 2 MiB limit.
36
37
  - ✍️ **Switch tasks without losing your draft.** Each conversation keeps its own draft, pasted images, and model settings in the current page. Copy or export results when you're done.
38
+ - 🖥️ **Run a command beside your task.** Open **Shell** to execute commands in the selected project's directory, follow stdout/stderr, interrupt a run, and reuse recent commands. The panel uses Codex's configured sandbox, with a ten-minute limit and no interactive input. Closing it keeps the command running. [Capabilities and limits](docs/shell.md).
37
39
 
38
40
  <details>
39
41
  <summary><strong>📱 A workspace that fits your phone</strong></summary>
package/README.zh-CN.md CHANGED
@@ -32,8 +32,10 @@
32
32
  - 📱 **离开电脑,也能跟进任务。** 在电脑或手机上查看工具活动与文件变更、处理权限审批、回答 Codex 的问题。关闭页面,任务仍会继续。
33
33
  - 🗂️ **多个项目,一眼掌握进展。** 让独立任务并行执行,随时看到哪些会话正在工作、等待审批,哪些已有结果可读。
34
34
  - 💬 **接着你的 Codex CLI 会话。** 浏览已有历史、恢复工作,或 Fork 一个新方向;为会话命名以便查找,完成后归档。
35
+ - ⚡ **更快打开长会话。** 优先显示最近消息,向上滚动时提前加载历史;本机已缓存的分片可先显示,再由服务端核对更新。[加载与缓存说明](docs/session-history.md)。
35
36
  - 📖 **一边看产出,一边继续讨论。** 电脑上将 Markdown 文档、JSON 文件与会话并排打开,继续向 Codex 提问;Markdown 支持代码、表格与图片,JSON 支持格式化缩进、语法高亮,格式无效时保留原文,可复制完整原文,手机上也有全屏阅读器。文件预览保持只读,Markdown 和 JSON 文件限制为 2 MiB。
36
37
  - ✍️ **切换任务,草稿还在。** 每个会话分别保留当前页面中的草稿、粘贴图片和模型设置;完成后可以复制或导出结果。
38
+ - 🖥️ **在任务旁运行命令。** 打开 **Shell**,在所选项目目录执行命令、查看实时标准输出和错误输出、中断执行,并复用近期命令。面板沿用 Codex 配置的沙箱,最长运行十分钟,不支持交互输入;关闭面板后命令仍继续运行。[能力与限制](docs/shell.md)。
37
39
 
38
40
  <details>
39
41
  <summary><strong>📱 手机上也能舒服地使用</strong></summary>
@@ -1,4 +1,5 @@
1
1
  import { CODEX_COMPATIBILITY } from "./compatibility.js";
2
+ import { SHELL_TIMEOUT_MS, SHELL_OUTPUT_BYTES } from "../shared/shell.js";
2
3
  import { spawn, execFile } from "node:child_process";
3
4
  import { randomUUID } from "node:crypto";
4
5
  import { promisify } from "node:util";
@@ -36,6 +37,8 @@ import {
36
37
  import { CodexError as CodexError2 } from "./transport.js";
37
38
  const VERIFIED_CODEX_VERSION = CODEX_COMPATIBILITY.recommended;
38
39
  const CAPABILITIES = [
40
+ "command/exec",
41
+ "command/exec/terminate",
39
42
  "thread/name/set",
40
43
  "thread/start",
41
44
  "thread/fork",
@@ -84,6 +87,7 @@ class CodexAdapter {
84
87
  this.options = options;
85
88
  }
86
89
  options;
90
+ commandOutputs = /* @__PURE__ */ new Map();
87
91
  epoch = randomUUID();
88
92
  child;
89
93
  rpc;
@@ -369,13 +373,13 @@ class CodexAdapter {
369
373
  });
370
374
  if (this.child === child) this.child = void 0;
371
375
  }
372
- async call(method, params, initializing = false) {
376
+ async call(method, params, initializing = false, timeoutMs) {
373
377
  if (!this.rpc || !initializing && !this.ready)
374
378
  throw new CodexError("CODEX_UNAVAILABLE", "Codex is not ready");
375
379
  if (this.disabled.has(method))
376
380
  throw new CodexError("PROTOCOL_INCOMPATIBLE", `Codex does not support ${method}`);
377
381
  try {
378
- return await this.rpc.request(method, params);
382
+ return await this.rpc.request(method, params, void 0, timeoutMs);
379
383
  } catch (error) {
380
384
  if (!initializing && error instanceof CodexError && error.rpcCode === -32601) {
381
385
  this.disabled.add(method);
@@ -532,6 +536,73 @@ class CodexAdapter {
532
536
  const sequence = ++this.archiveSequence;
533
537
  return this.storedThread(await this.readThreadHeader(id), sequence);
534
538
  }
539
+ async readThreadPage(id, continuation) {
540
+ const epoch = this.epoch;
541
+ let cursor = null;
542
+ if (continuation) {
543
+ try {
544
+ const decoded = JSON.parse(Buffer.from(continuation, "base64url").toString("utf8"));
545
+ if (decoded.threadId !== id || decoded.epoch !== epoch || typeof decoded.cursor !== "string")
546
+ throw new Error("Invalid cursor");
547
+ cursor = decoded.cursor;
548
+ } catch {
549
+ throw new AppError("HISTORY_CURSOR_EXPIRED", "\u5386\u53F2\u5206\u9875\u5DF2\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u540C\u6B65\u4F1A\u8BDD\u3002", 409);
550
+ }
551
+ }
552
+ const sequence = ++this.archiveSequence;
553
+ const header = await this.readThreadHeader(id);
554
+ if (header.historyMode !== "paginated") return this.readThread(id);
555
+ const page = await this.call("thread/turns/list", {
556
+ threadId: id,
557
+ cursor,
558
+ limit: 12,
559
+ sortDirection: "desc",
560
+ itemsView: "summary"
561
+ });
562
+ if (page.nextCursor && page.nextCursor === cursor)
563
+ throw new CodexError("PROTOCOL_INCOMPATIBLE", "Invalid turn pagination");
564
+ await this.hydrateTurns(id, page.data);
565
+ if (this.epoch !== epoch)
566
+ throw new AppError("HISTORY_CURSOR_EXPIRED", "\u8BFB\u53D6\u671F\u95F4 Codex \u5DF2\u91CD\u542F\uFF0C\u8BF7\u91CD\u65B0\u540C\u6B65\u4F1A\u8BDD\u3002", 409);
567
+ const view = await this.storedThread(
568
+ { ...header, turns: page.data.slice().reverse() },
569
+ sequence
570
+ );
571
+ return {
572
+ ...view,
573
+ history: {
574
+ olderCursor: page.nextCursor ? Buffer.from(
575
+ JSON.stringify({
576
+ threadId: id,
577
+ epoch,
578
+ cursor: page.nextCursor
579
+ })
580
+ ).toString("base64url") : null
581
+ }
582
+ };
583
+ }
584
+ async hydrateTurns(id, turns) {
585
+ for (const turn of turns) {
586
+ if (turn.itemsView === "full") continue;
587
+ let cursor = null;
588
+ const seen = /* @__PURE__ */ new Set();
589
+ turn.items = [];
590
+ do {
591
+ const page = await this.call("thread/items/list", {
592
+ threadId: id,
593
+ turnId: turn.id,
594
+ cursor,
595
+ limit: 20,
596
+ sortDirection: "asc"
597
+ });
598
+ turn.items.push(...page.data.map((entry) => entry.item));
599
+ cursor = page.nextCursor;
600
+ if (cursor && (seen.has(cursor) || seen.size >= 1e3))
601
+ throw new CodexError("PROTOCOL_INCOMPATIBLE", "Invalid item pagination");
602
+ if (cursor) seen.add(cursor);
603
+ } while (cursor);
604
+ }
605
+ }
535
606
  async readThread(id) {
536
607
  const sequence = ++this.archiveSequence;
537
608
  const thread = await this.readThreadHeader(id);
@@ -722,7 +793,7 @@ class CodexAdapter {
722
793
  throw error;
723
794
  }
724
795
  titles?.register(result);
725
- const view = await this.readThread(result.thread.id);
796
+ const view = await this.readThreadPage(result.thread.id);
726
797
  return { ...view, runtimeSettings: await this.responseRuntimeSettings(result) };
727
798
  }
728
799
  async forkThread(id, cwd, settings) {
@@ -801,11 +872,46 @@ class CodexAdapter {
801
872
  async compactThread(id) {
802
873
  await this.call("thread/compact/start", { threadId: id });
803
874
  }
875
+ async executeCommand(processId, cwd, command, output) {
876
+ this.commandOutputs.set(processId, output);
877
+ try {
878
+ return await this.call(
879
+ "command/exec",
880
+ {
881
+ processId,
882
+ cwd,
883
+ command: process.platform === "win32" ? ["cmd.exe", "/d", "/s", "/c", command] : ["/bin/sh", "-c", command],
884
+ streamStdoutStderr: true,
885
+ streamStdin: false,
886
+ tty: false,
887
+ timeoutMs: SHELL_TIMEOUT_MS,
888
+ outputBytesCap: SHELL_OUTPUT_BYTES / 2
889
+ // Omit sandboxPolicy: preserve the server's configured permission policy.
890
+ },
891
+ false,
892
+ SHELL_TIMEOUT_MS + 5e3
893
+ );
894
+ } finally {
895
+ this.commandOutputs.delete(processId);
896
+ }
897
+ }
898
+ async terminateCommand(processId) {
899
+ await this.call("command/exec/terminate", { processId });
900
+ }
804
901
  notification(message) {
805
902
  if (this.titles?.notification(message)) return;
806
903
  const event = message;
807
904
  const p = event.params;
808
905
  switch (event.method) {
906
+ case "command/exec/outputDelta": {
907
+ const { processId, stream, deltaBase64, capReached } = event.params;
908
+ this.commandOutputs.get(processId)?.(
909
+ stream,
910
+ Buffer.from(deltaBase64, "base64"),
911
+ capReached
912
+ );
913
+ break;
914
+ }
809
915
  case "thread/settings/updated":
810
916
  {
811
917
  const observed = {