@tt-a1i/openpi 0.6.0 → 0.7.0

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.
Files changed (60) hide show
  1. package/README.md +13 -11
  2. package/SETUP.md +2 -2
  3. package/THIRD_PARTY_NOTICES.md +242 -0
  4. package/extensions/ai-providers/README.md +12 -6
  5. package/extensions/ai-providers/cursor/connect-frame-reader.ts +76 -0
  6. package/extensions/ai-providers/cursor/input-images.ts +2 -3
  7. package/extensions/ai-providers/cursor/proto.ts +218 -11
  8. package/extensions/ai-providers/cursor/protobuf.ts +12 -2
  9. package/extensions/ai-providers/cursor/provider.ts +280 -37
  10. package/extensions/ai-providers/cursor/tool-bridge.ts +68 -0
  11. package/extensions/ai-providers/index.ts +3 -3
  12. package/extensions/file-mutation-display/index.ts +13 -9
  13. package/extensions/shared/agent-transcript.ts +3 -2
  14. package/extensions/shared/child-session.ts +14 -0
  15. package/extensions/subagents/index.ts +20 -3
  16. package/extensions/subagents/src/agent-types.ts +5 -17
  17. package/extensions/subagents/src/backends/pi.ts +70 -59
  18. package/extensions/subagents/src/backends/tool-preview.ts +29 -0
  19. package/extensions/subagents/src/manager.ts +2 -71
  20. package/extensions/subagents/src/prompt.ts +2 -2
  21. package/extensions/subagents/src/runtime.ts +10 -3
  22. package/extensions/web/index.ts +69 -5
  23. package/extensions/workflows/artifacts.ts +362 -23
  24. package/extensions/workflows/dashboard.ts +141 -21
  25. package/extensions/workflows/index.ts +62 -20
  26. package/extensions/workflows/progress-projection.ts +7 -1
  27. package/extensions/workflows/runner.ts +5 -162
  28. package/extensions/workflows/sandbox.ts +4 -0
  29. package/package.json +28 -4
  30. package/skills/subagents/REFERENCE.md +6 -7
  31. package/skills/subagents/SKILL.md +1 -1
  32. package/skills/workflows/REFERENCE.md +3 -1
  33. package/web/dist/app.js +87 -0
  34. package/web/dist/favicon.svg +9 -0
  35. package/web/dist/index.html +15 -0
  36. package/web/dist/styles.css +3 -0
  37. package/web/host/web-host.ts +6 -9
  38. package/web/ui/index.html +3 -131
  39. package/web/ui/public/favicon.svg +9 -0
  40. package/web/ui/src/app/App.tsx +134 -0
  41. package/web/ui/src/app/providers.tsx +38 -0
  42. package/web/ui/src/components/Markdown.tsx +58 -0
  43. package/web/ui/src/components/OpenPiLogo.tsx +41 -0
  44. package/web/ui/src/features/activity/ActivityBar.tsx +120 -0
  45. package/web/ui/src/features/composer/Composer.tsx +237 -0
  46. package/web/ui/src/features/sessions/SessionSidebar.tsx +418 -0
  47. package/web/ui/src/features/transcript/Transcript.tsx +860 -0
  48. package/web/ui/src/i18n.ts +159 -0
  49. package/web/ui/src/lib/format.ts +57 -0
  50. package/web/ui/src/main.tsx +16 -0
  51. package/web/ui/src/protocol/client.ts +199 -0
  52. package/web/ui/src/protocol/event-stream.ts +88 -0
  53. package/web/ui/src/store/web-store.ts +926 -0
  54. package/web/ui/src/styles.css +420 -0
  55. package/web/ui/tsconfig.json +12 -0
  56. package/web/ui/vite-env.d.ts +1 -0
  57. package/web/vite.config.mjs +21 -1
  58. package/web/host/static-assets.ts +0 -4
  59. package/web/ui/app.js +0 -1700
  60. package/web/ui/styles.css +0 -680
@@ -0,0 +1,159 @@
1
+ import i18n from "i18next";
2
+ import { initReactI18next } from "react-i18next";
3
+
4
+ const resources = {
5
+ en: {
6
+ translation: {
7
+ activeOnlyHint: "Only the active Web session accepts messages.",
8
+ acceptedHint: "Message accepted by OpenPI Web.",
9
+ stopTurn: "Stop current turn",
10
+ stoppingTurn: "Stopping current turn…",
11
+ stoppedTurn: "Current turn stopped.",
12
+ pendingFollowUpsHint: "{{count}} messages queued",
13
+ addWorkspace: "Add workspace",
14
+ addWorkspaceMenu: "Add workspace...",
15
+ archiveConversation: "Archive conversation",
16
+ cancel: "Cancel",
17
+ chooseWorkspaceHint: "Choose a workspace and describe the work",
18
+ close: "Close",
19
+ closeSearch: "Close search",
20
+ collapseSidebar: "Collapse sidebar",
21
+ confirmEdit: "OK",
22
+ connected: "Connected",
23
+ connecting: "Connecting",
24
+ conversationName: "Conversation name",
25
+ conversationOptions: "Conversation options",
26
+ conversationTurns: "Conversation turns",
27
+ copiedMessage: "Copied",
28
+ copyMessage: "Copy message",
29
+ deleteWorkspace: "Delete workspace",
30
+ describeTask: "Describe a task",
31
+ editMessage: "Edit message",
32
+ enterHint: "Enter to send, Shift+Enter for a new line.",
33
+ expandSidebar: "Expand sidebar",
34
+ importWorkspace: "Import workspace",
35
+ loadingModels: "Loading models...",
36
+ modelPreparing: "Preparing task...",
37
+ modelRetrying: "Retrying model request...",
38
+ modelRunning: "Working...",
39
+ newSession: "New session",
40
+ noConversations: "No conversations yet",
41
+ noMatching: "No matching conversations",
42
+ noModels: "No models available",
43
+ noOutput: "no output",
44
+ noSessions: "No sessions",
45
+ openSidebar: "Open sidebar",
46
+ promptMessage: "Send a message to the active Web session",
47
+ promptReadonly: "A non-active session cannot receive prompts",
48
+ promptStart: "Choose a workspace to begin.",
49
+ promptTask: "Describe what you want to build.",
50
+ queuedHint: "The message will be queued after the current turn.",
51
+ reconnecting: "Reconnecting",
52
+ removeWorkspace: "Remove from sidebar",
53
+ renameConversation: "Rename conversation",
54
+ renameWorkspace: "Rename workspace",
55
+ save: "Save",
56
+ searchConversations: "Search conversations",
57
+ searchPlaceholder: "Search conversations...",
58
+ selectModel: "Select model",
59
+ selectWorkspace: "Select workspace",
60
+ send: "Send",
61
+ stepsLabel: "steps",
62
+ switchingSession: "Switching session...",
63
+ thinkingActive: "Thinking...",
64
+ thinkingDone: "Thinking",
65
+ unavailable: "Unavailable",
66
+ ungrouped: "Ungrouped",
67
+ untitledSession: "New session",
68
+ workspaceDeleteConfirm:
69
+ "The folder and conversation records will be kept. Its conversations will move to Ungrouped.",
70
+ workspaceName: "Workspace name",
71
+ workspaces: "Workspaces",
72
+ },
73
+ },
74
+ zh: {
75
+ translation: {
76
+ activeOnlyHint: "只有当前 Web 会话可以接收消息。",
77
+ stopTurn: "停止当前轮次",
78
+ stoppingTurn: "正在停止当前轮次…",
79
+ stoppedTurn: "当前轮次已停止。",
80
+ pendingFollowUpsHint: "{{count}} 条消息正在排队",
81
+ acceptedHint: "OpenPI Web 已接收消息。",
82
+ addWorkspace: "添加工作区",
83
+ addWorkspaceMenu: "添加工作区...",
84
+ archiveConversation: "归档会话",
85
+ cancel: "取消",
86
+ chooseWorkspaceHint: "选择工作区并描述任务",
87
+ close: "关闭",
88
+ closeSearch: "关闭搜索",
89
+ collapseSidebar: "收起侧边栏",
90
+ confirmEdit: "确定",
91
+ connected: "已连接",
92
+ connecting: "正在连接",
93
+ conversationName: "会话名称",
94
+ conversationOptions: "会话选项",
95
+ conversationTurns: "会话轮次",
96
+ copiedMessage: "已复制",
97
+ copyMessage: "复制消息",
98
+ deleteWorkspace: "删除工作区",
99
+ describeTask: "描述任务",
100
+ editMessage: "编辑消息",
101
+ enterHint: "按 Enter 发送,Shift+Enter 换行。",
102
+ expandSidebar: "展开侧边栏",
103
+ importWorkspace: "导入工作区",
104
+ loadingModels: "正在加载模型...",
105
+ modelPreparing: "正在准备任务...",
106
+ modelRetrying: "模型请求重试中...",
107
+ modelRunning: "正在运行...",
108
+ newSession: "新建会话",
109
+ noConversations: "暂无对话",
110
+ noMatching: "没有匹配的会话",
111
+ noModels: "没有可用模型",
112
+ noOutput: "无输出",
113
+ noSessions: "暂无会话",
114
+ openSidebar: "打开侧边栏",
115
+ promptMessage: "向当前 Web 会话发送消息",
116
+ promptReadonly: "非当前会话不能接收消息",
117
+ promptStart: "选择一个工作区开始",
118
+ promptTask: "描述你想要构建的任务",
119
+ queuedHint: "当前回合结束后将发送消息。",
120
+ reconnecting: "正在重连",
121
+ removeWorkspace: "从侧边栏移除",
122
+ renameConversation: "重命名会话",
123
+ renameWorkspace: "重命名工作区",
124
+ save: "保存",
125
+ searchConversations: "搜索会话",
126
+ searchPlaceholder: "搜索会话...",
127
+ selectModel: "选择模型",
128
+ selectWorkspace: "选择工作区",
129
+ send: "发送",
130
+ stepsLabel: "个步骤",
131
+ switchingSession: "正在切换会话...",
132
+ thinkingActive: "思考中...",
133
+ thinkingDone: "思考过程",
134
+ unavailable: "不可用",
135
+ ungrouped: "未分组",
136
+ untitledSession: "新会话",
137
+ workspaceDeleteConfirm:
138
+ "文件夹与会话记录会保留,其中的会话会被放到“未分组”;再次打开此目录时将是一个干净的工作区。",
139
+ workspaceName: "工作区名称",
140
+ workspaces: "工作区",
141
+ },
142
+ },
143
+ } as const;
144
+
145
+ const language = navigator.language?.toLowerCase().startsWith("zh")
146
+ ? "zh"
147
+ : "en";
148
+
149
+ void i18n.use(initReactI18next).init({
150
+ fallbackLng: "en",
151
+ initAsync: false,
152
+ interpolation: { escapeValue: false },
153
+ lng: language,
154
+ resources,
155
+ });
156
+
157
+ document.documentElement.lang = language === "zh" ? "zh-CN" : "en";
158
+
159
+ export { i18n };
@@ -0,0 +1,57 @@
1
+ import type { WebSessionSummary } from "../../../protocol/types.ts";
2
+
3
+ export function sessionTitle(
4
+ session: Partial<WebSessionSummary>,
5
+ fallback: string,
6
+ ) {
7
+ return session.name?.trim() || session.firstMessage?.trim() || fallback;
8
+ }
9
+
10
+ export function workspaceName(path: string) {
11
+ return path.split(/[\\/]/u).filter(Boolean).at(-1) || path;
12
+ }
13
+
14
+ export function compactSummary(value: unknown, limit = 96) {
15
+ const text = String(value ?? "")
16
+ .replace(/\s+/gu, " ")
17
+ .trim();
18
+ return text.length > limit ? `${text.slice(0, limit - 1)}…` : text;
19
+ }
20
+
21
+ export function relativeTime(value: string) {
22
+ const elapsed = Date.now() - new Date(value).getTime();
23
+ if (!Number.isFinite(elapsed)) return "";
24
+ if (elapsed < 60_000) return "now";
25
+ if (elapsed < 3_600_000) return `${Math.floor(elapsed / 60_000)}m`;
26
+ if (elapsed < 86_400_000) return `${Math.floor(elapsed / 3_600_000)}h`;
27
+ return new Date(value).toLocaleDateString(undefined, {
28
+ day: "numeric",
29
+ month: "short",
30
+ });
31
+ }
32
+
33
+ export function formatTurnTime(timestamp?: string) {
34
+ if (!timestamp) return "";
35
+ const date = new Date(timestamp);
36
+ if (Number.isNaN(date.getTime())) return "";
37
+ return `${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`;
38
+ }
39
+
40
+ export function formatElapsedMs(start?: number, end = Date.now()) {
41
+ if (typeof start !== "number") return "";
42
+ const totalSeconds = Math.max(0, Math.round((end - start) / 1_000));
43
+ const minutes = Math.floor(totalSeconds / 60);
44
+ const seconds = totalSeconds % 60;
45
+ return minutes > 0
46
+ ? `${minutes}m${String(seconds).padStart(2, "0")}s`
47
+ : `${seconds}s`;
48
+ }
49
+
50
+ export function turnTitle(content: string) {
51
+ const line =
52
+ content
53
+ .split("\n")
54
+ .map((part) => part.trim())
55
+ .find(Boolean) ?? "";
56
+ return line.length > 60 ? `${line.slice(0, 60)}…` : line;
57
+ }
@@ -0,0 +1,16 @@
1
+ import React from "react";
2
+ import { createRoot } from "react-dom/client";
3
+ import { App } from "./app/App.tsx";
4
+ import { Providers } from "./app/providers.tsx";
5
+ import "./styles.css";
6
+
7
+ const root = document.getElementById("root");
8
+ if (!root) throw new Error("OpenPI Web root is missing");
9
+
10
+ createRoot(root).render(
11
+ <React.StrictMode>
12
+ <Providers>
13
+ <App />
14
+ </Providers>
15
+ </React.StrictMode>,
16
+ );
@@ -0,0 +1,199 @@
1
+ import type { WebSnapshot } from "../../../protocol/types.ts";
2
+
3
+ const tokenStorageKey = "openpi.web.token";
4
+
5
+ export class WebApiError extends Error {
6
+ constructor(
7
+ message: string,
8
+ readonly status: number,
9
+ readonly code?: string,
10
+ ) {
11
+ super(message);
12
+ this.name = "WebApiError";
13
+ }
14
+ }
15
+
16
+ function readToken() {
17
+ const fragmentToken = new URLSearchParams(location.hash.slice(1)).get(
18
+ "token",
19
+ );
20
+ if (fragmentToken) {
21
+ try {
22
+ window.sessionStorage.setItem(tokenStorageKey, fragmentToken);
23
+ } catch {}
24
+ history.replaceState(null, "", `${location.pathname}${location.search}`);
25
+ return fragmentToken;
26
+ }
27
+ try {
28
+ return window.sessionStorage.getItem(tokenStorageKey);
29
+ } catch {
30
+ return null;
31
+ }
32
+ }
33
+
34
+ export interface CommandReceipt {
35
+ id: string;
36
+ accepted: boolean;
37
+ pendingFollowUps?: number;
38
+ }
39
+
40
+ export interface SessionMutationResult {
41
+ cancelled?: boolean;
42
+ path?: string;
43
+ }
44
+
45
+ export interface WorkspaceSelectionResult {
46
+ cancelled?: boolean;
47
+ path?: string;
48
+ }
49
+
50
+ export class WebClient {
51
+ readonly token = readToken();
52
+
53
+ headers(json = false) {
54
+ return {
55
+ Authorization: `Bearer ${this.token ?? ""}`,
56
+ ...(json ? { "Content-Type": "application/json" } : {}),
57
+ };
58
+ }
59
+
60
+ async request<T>(
61
+ path: string,
62
+ options: RequestInit & { timeoutMs?: number; timeoutMessage?: string } = {},
63
+ ) {
64
+ const {
65
+ timeoutMs = 15_000,
66
+ timeoutMessage = "Request timed out. Please try again.",
67
+ ...requestOptions
68
+ } = options;
69
+ const controller = new AbortController();
70
+ let timedOut = false;
71
+ const abort = () => controller.abort();
72
+ if (options.signal?.aborted) abort();
73
+ options.signal?.addEventListener("abort", abort, { once: true });
74
+ const timer = window.setTimeout(() => {
75
+ timedOut = true;
76
+ controller.abort();
77
+ }, timeoutMs);
78
+ try {
79
+ const response = await fetch(path, {
80
+ ...requestOptions,
81
+ signal: controller.signal,
82
+ headers: { ...this.headers(Boolean(options.body)), ...options.headers },
83
+ });
84
+ const body = (await response.json()) as {
85
+ error?: string;
86
+ code?: string;
87
+ } & T;
88
+ if (controller.signal.aborted) throw new Error("Request aborted");
89
+ if (!response.ok)
90
+ throw new WebApiError(
91
+ body.error || `Request failed (${response.status})`,
92
+ response.status,
93
+ body.code,
94
+ );
95
+ return body;
96
+ } catch (error) {
97
+ if (timedOut) throw new Error(timeoutMessage);
98
+ throw error;
99
+ } finally {
100
+ window.clearTimeout(timer);
101
+ options.signal?.removeEventListener("abort", abort);
102
+ }
103
+ }
104
+
105
+ snapshot(path?: string | null) {
106
+ const suffix = path ? `?path=${encodeURIComponent(path)}` : "";
107
+ return this.request<WebSnapshot>(`/api/snapshot${suffix}`);
108
+ }
109
+
110
+ chooseWorkspace() {
111
+ return this.request<WorkspaceSelectionResult>("/api/workspaces/select", {
112
+ method: "POST",
113
+ });
114
+ }
115
+
116
+ renameWorkspace(path: string, name: string) {
117
+ return this.request<{ path: string; name: string }>("/api/workspaces", {
118
+ method: "PATCH",
119
+ body: JSON.stringify({ path, name }),
120
+ });
121
+ }
122
+
123
+ removeWorkspace(path: string) {
124
+ return this.request<{ path: string; removed: true }>(
125
+ `/api/workspaces?path=${encodeURIComponent(path)}`,
126
+ { method: "DELETE" },
127
+ );
128
+ }
129
+
130
+ createSession(workspacePath: string, commandId: string) {
131
+ return this.request<SessionMutationResult>("/api/sessions", {
132
+ method: "POST",
133
+ body: JSON.stringify({ workspacePath, commandId }),
134
+ });
135
+ }
136
+
137
+ selectSession(path: string) {
138
+ return this.request<SessionMutationResult>("/api/sessions/select", {
139
+ method: "POST",
140
+ body: JSON.stringify({ path }),
141
+ });
142
+ }
143
+
144
+ renameSession(path: string, name: string) {
145
+ return this.request<{ path: string; name: string }>("/api/sessions", {
146
+ method: "PATCH",
147
+ body: JSON.stringify({ path, name }),
148
+ });
149
+ }
150
+
151
+ archiveSession(path: string) {
152
+ return this.request<{ path: string; archived: true }>(
153
+ `/api/sessions/archive?path=${encodeURIComponent(path)}`,
154
+ { method: "POST" },
155
+ );
156
+ }
157
+
158
+ selectModel(provider: string, modelId: string, sessionId: string) {
159
+ return this.request<CommandReceipt>("/api/model", {
160
+ method: "POST",
161
+ body: JSON.stringify({ provider, modelId, sessionId }),
162
+ });
163
+ }
164
+
165
+ cancelActiveTurn(turn: NonNullable<WebSnapshot["runtime"]["activeTurn"]>) {
166
+ return this.request<{ state: "accepted" | "already-settled" }>(
167
+ "/api/turns/cancel",
168
+ {
169
+ method: "POST",
170
+ body: JSON.stringify(turn),
171
+ },
172
+ );
173
+ }
174
+
175
+ async prompt(
176
+ sessionId: string,
177
+ content: string,
178
+ commandId: string,
179
+ retry = false,
180
+ ) {
181
+ const receipt = await this.request<CommandReceipt>("/api/prompt", {
182
+ method: "POST",
183
+ body: JSON.stringify({ sessionId, content, commandId, retry }),
184
+ timeoutMs: 30_000,
185
+ timeoutMessage:
186
+ "Request timed out; admission may still be pending. Retry the same message to recover its receipt.",
187
+ });
188
+ if (
189
+ typeof receipt.id !== "string" ||
190
+ !receipt.id ||
191
+ receipt.accepted !== true
192
+ ) {
193
+ throw new Error(
194
+ "Invalid prompt receipt; retry the same message to recover its admission.",
195
+ );
196
+ }
197
+ return receipt;
198
+ }
199
+ }
@@ -0,0 +1,88 @@
1
+ import { createParser } from "eventsource-parser";
2
+ import type { WebEvent } from "../../../protocol/types.ts";
3
+ import type { WebClient } from "./client.ts";
4
+
5
+ export class EventResyncRequired extends Error {}
6
+
7
+ export interface EventStreamOptions {
8
+ client: WebClient;
9
+ cursor: number;
10
+ onConnected: () => void;
11
+ onHeartbeat?: () => void;
12
+ onEvent: (event: WebEvent) => void;
13
+ signal: AbortSignal;
14
+ }
15
+
16
+ export async function consumeEventStream(options: EventStreamOptions) {
17
+ const controller = new AbortController();
18
+ const abort = () => controller.abort();
19
+ options.signal.addEventListener("abort", abort, { once: true });
20
+ if (options.signal.aborted) abort();
21
+ const timer = window.setTimeout(abort, 45_000);
22
+ const response = await fetch(`/events?cursor=${options.cursor}`, {
23
+ headers: options.client.headers(),
24
+ signal: controller.signal,
25
+ }).finally(() => {
26
+ window.clearTimeout(timer);
27
+ options.signal.removeEventListener("abort", abort);
28
+ });
29
+ if (response.status === 409)
30
+ throw new EventResyncRequired("event replay expired");
31
+ if (!response.ok || !response.body)
32
+ throw new Error("event connection failed");
33
+ options.onConnected();
34
+
35
+ let cursor = options.cursor;
36
+ let heartbeats = 0;
37
+ const parser = createParser({
38
+ onComment(comment) {
39
+ if (comment.trim() === "heartbeat" && ++heartbeats >= 4) {
40
+ heartbeats = 0;
41
+ options.onHeartbeat?.();
42
+ }
43
+ },
44
+ onEvent(record) {
45
+ const event = JSON.parse(record.data) as WebEvent;
46
+ if (!Number.isSafeInteger(event.sequence)) {
47
+ throw new EventResyncRequired("invalid event cursor");
48
+ }
49
+ if (event.sequence <= cursor) return;
50
+ if (event.sequence !== cursor + 1) {
51
+ throw new EventResyncRequired("event cursor gap");
52
+ }
53
+ cursor = event.sequence;
54
+ if (event.type === "state_invalidated") {
55
+ throw new EventResyncRequired("state invalidated");
56
+ }
57
+ options.onEvent(event);
58
+ },
59
+ });
60
+ const reader = response.body.getReader();
61
+ const decoder = new TextDecoder();
62
+ try {
63
+ while (!options.signal.aborted) {
64
+ let timer: number | undefined;
65
+ let onAbort: (() => void) | undefined;
66
+ const chunk = reader.read();
67
+ const interruption = new Promise<never>((_, reject) => {
68
+ onAbort = () => reject(new Error("event stream aborted"));
69
+ options.signal.addEventListener("abort", onAbort, { once: true });
70
+ if (options.signal.aborted) onAbort();
71
+ timer = window.setTimeout(
72
+ () => reject(new Error("event stream stalled")),
73
+ 45_000,
74
+ );
75
+ });
76
+ const { done, value } = await Promise.race([chunk, interruption]).finally(
77
+ () => {
78
+ window.clearTimeout(timer);
79
+ if (onAbort) options.signal.removeEventListener("abort", onAbort);
80
+ },
81
+ );
82
+ if (done) throw new Error("event connection closed");
83
+ parser.feed(decoder.decode(value, { stream: true }));
84
+ }
85
+ } finally {
86
+ await reader.cancel().catch(() => undefined);
87
+ }
88
+ }