@tt-a1i/openpi 0.5.0 → 0.6.1
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 +30 -20
- package/SETUP.md +10 -4
- package/THIRD_PARTY_NOTICES.md +16 -0
- package/bin/openpi.js +25 -15
- package/extensions/ai-providers/LICENSE.upstream +23 -0
- package/extensions/ai-providers/README.md +65 -0
- package/extensions/ai-providers/antigravity/credentials.ts +52 -0
- package/extensions/ai-providers/antigravity/discovery.ts +130 -0
- package/extensions/ai-providers/antigravity/google-conversion.ts +455 -0
- package/extensions/ai-providers/antigravity/models.ts +84 -0
- package/extensions/ai-providers/antigravity/oauth.ts +700 -0
- package/extensions/ai-providers/antigravity/provider.ts +1116 -0
- package/extensions/ai-providers/antigravity/routing.ts +340 -0
- package/extensions/ai-providers/antigravity/with-resolvers.d.ts +19 -0
- package/extensions/ai-providers/cursor/constants.ts +5 -0
- package/extensions/ai-providers/cursor/credentials.ts +14 -0
- package/extensions/ai-providers/cursor/discovery.ts +291 -0
- package/extensions/ai-providers/cursor/input-images.ts +105 -0
- package/extensions/ai-providers/cursor/models.ts +45 -0
- package/extensions/ai-providers/cursor/oauth.ts +263 -0
- package/extensions/ai-providers/cursor/proto.ts +1271 -0
- package/extensions/ai-providers/cursor/protobuf.ts +1181 -0
- package/extensions/ai-providers/cursor/provider.ts +1431 -0
- package/extensions/ai-providers/cursor/proxy.ts +213 -0
- package/extensions/ai-providers/cursor/tool-bridge.ts +68 -0
- package/extensions/ai-providers/cursor/with-resolvers.d.ts +12 -0
- package/extensions/ai-providers/index.ts +86 -0
- package/extensions/ai-providers/oauth-adapter.ts +81 -0
- package/extensions/ai-providers/usage.ts +10 -0
- package/extensions/background-terminals/index.ts +8 -1
- package/extensions/background-terminals/src/manager.ts +3 -5
- package/extensions/background-terminals/src/result-delivery.ts +43 -23
- package/extensions/cron/index.ts +68 -27
- package/extensions/cron/schedule.ts +5 -1
- package/extensions/model-info/cache-diagnostics.ts +220 -0
- package/extensions/model-info/index.ts +45 -1
- package/extensions/plan-mode/index.ts +75 -4
- package/extensions/setup/index.ts +15 -3
- package/extensions/shared/child-session.ts +39 -5
- package/extensions/shared/completion-inbox.ts +193 -0
- package/extensions/shared/setup-config.ts +10 -1
- package/extensions/shared/structured-output.ts +154 -0
- package/extensions/subagents/index.ts +64 -7
- package/extensions/subagents/src/agent-types.ts +5 -17
- package/extensions/subagents/src/backends/pi.ts +130 -48
- package/extensions/subagents/src/backends/tool-preview.ts +29 -0
- package/extensions/subagents/src/domain.ts +16 -1
- package/extensions/subagents/src/manager.ts +7 -71
- package/extensions/subagents/src/prompt.ts +19 -5
- package/extensions/subagents/src/result-artifact.ts +32 -0
- package/extensions/subagents/src/result-delivery.ts +33 -14
- package/extensions/subagents/src/runtime.ts +10 -3
- package/extensions/ui-customization/footer.ts +16 -5
- package/extensions/user-input-fold/index.ts +42 -6
- package/extensions/web/index.ts +25 -2
- package/extensions/workflows/acceptance.ts +43 -19
- package/extensions/workflows/completion-projection.ts +3 -1
- package/extensions/workflows/dashboard.ts +147 -21
- package/extensions/workflows/index.ts +75 -20
- package/extensions/workflows/model.ts +5 -1
- package/extensions/workflows/progress-projection.ts +7 -1
- package/extensions/workflows/prompt.ts +4 -10
- package/extensions/workflows/result-delivery.ts +96 -22
- package/extensions/workflows/retention.ts +6 -0
- package/extensions/workflows/runner.ts +11 -233
- package/extensions/workflows/sandbox.ts +4 -0
- package/package.json +7 -7
- package/skills/subagents/REFERENCE.md +9 -9
- package/skills/subagents/SKILL.md +2 -1
- package/skills/workflows/REFERENCE.md +5 -3
- package/skills/workflows/SKILL.md +1 -1
- package/web/adapter/pi-adapter.ts +3 -0
- package/web/host/pi-coding-agent-entry.ts +162 -0
- package/web/host/web-host.ts +330 -50
- package/web/protocol/types.ts +5 -0
- package/web/runtime/pi-runtime.ts +240 -25
- package/web/runtime/types.ts +32 -1
- package/web/ui/app.js +343 -41
- package/web/ui/index.html +3 -0
- package/web/ui/styles.css +119 -37
package/web/ui/app.js
CHANGED
|
@@ -19,9 +19,13 @@ const state = {
|
|
|
19
19
|
collapsed: readCollapsedWorkspaces(),
|
|
20
20
|
liveMessages: [],
|
|
21
21
|
liveRunning: false,
|
|
22
|
+
activeTurn: null,
|
|
23
|
+
turnCancellationPending: false,
|
|
24
|
+
turnTerminalStatus: null,
|
|
22
25
|
promptAdmissionPending: false,
|
|
23
26
|
promptAdmissionToken: null,
|
|
24
27
|
promptAdmissionSequence: 0,
|
|
28
|
+
promptAdmission: null,
|
|
25
29
|
terminalPromptIds: new Set(),
|
|
26
30
|
sessionEpoch: 0,
|
|
27
31
|
sessionSwitching: false,
|
|
@@ -30,6 +34,9 @@ const state = {
|
|
|
30
34
|
snapshotGeneration: 0,
|
|
31
35
|
livePhase: "idle",
|
|
32
36
|
liveRetry: null,
|
|
37
|
+
composerFeedback: null,
|
|
38
|
+
pendingFollowUpsReceipt: null,
|
|
39
|
+
themePreference: "system",
|
|
33
40
|
query: "",
|
|
34
41
|
selectedWorkspace: null,
|
|
35
42
|
language: navigator.language?.toLowerCase().startsWith("zh") ? "zh" : "en",
|
|
@@ -67,6 +74,14 @@ const translations = {
|
|
|
67
74
|
enterHint: "Enter to send, Shift+Enter for a new line.",
|
|
68
75
|
activeOnlyHint: "Only the active Web session accepts messages.",
|
|
69
76
|
acceptedHint: "Message accepted by OpenPI Web.",
|
|
77
|
+
admissionPendingHint: "OpenPI is still accepting the previous message.",
|
|
78
|
+
admissionTimeout: "Prompt admission timed out. Retrying will check the same message.",
|
|
79
|
+
requestTimeout: "The Web request timed out.",
|
|
80
|
+
reconnectingHint: "Live updates were interrupted. Reconnecting and checking canonical state...",
|
|
81
|
+
pendingFollowUpsHint: "Message received; {count} follow-up messages were waiting when it was received.",
|
|
82
|
+
stopTurn: "Stop turn",
|
|
83
|
+
stoppingTurn: "Stopping current turn...",
|
|
84
|
+
stoppedTurn: "Current turn stopped.",
|
|
70
85
|
modelRunning: "Working...",
|
|
71
86
|
modelPreparing: "Preparing task...",
|
|
72
87
|
modelRetrying: "Retrying model request...",
|
|
@@ -103,6 +118,14 @@ const translations = {
|
|
|
103
118
|
enterHint: "按 Enter 发送,Shift+Enter 换行。",
|
|
104
119
|
activeOnlyHint: "只有当前 Web 会话可以接收消息。",
|
|
105
120
|
acceptedHint: "OpenPI Web 已接收消息。",
|
|
121
|
+
admissionPendingHint: "OpenPI 仍在接收上一条消息,请稍候。",
|
|
122
|
+
admissionTimeout: "消息接收超时;重试会核对同一条消息。",
|
|
123
|
+
requestTimeout: "Web 请求已超时。",
|
|
124
|
+
reconnectingHint: "实时更新已中断,正在重连并核对权威状态……",
|
|
125
|
+
pendingFollowUpsHint: "消息已接收;接收时有 {count} 条后续消息等待处理。",
|
|
126
|
+
stopTurn: "停止当前回合",
|
|
127
|
+
stoppingTurn: "正在停止当前回合...",
|
|
128
|
+
stoppedTurn: "当前回合已停止。",
|
|
106
129
|
modelRunning: "正在运行...",
|
|
107
130
|
modelPreparing: "正在准备任务...",
|
|
108
131
|
modelRetrying: "模型请求重试中...",
|
|
@@ -126,7 +149,24 @@ function applyLanguage() {
|
|
|
126
149
|
}
|
|
127
150
|
|
|
128
151
|
const $ = (id) => document.getElementById(id);
|
|
152
|
+
const supportedThemes = new Set(["system", "light", "dark"]);
|
|
153
|
+
const systemTheme = window.matchMedia?.("(prefers-color-scheme: dark)");
|
|
154
|
+
function applyThemePreference(preference) {
|
|
155
|
+
state.themePreference = supportedThemes.has(preference) ? preference : "system";
|
|
156
|
+
const resolved = state.themePreference === "system"
|
|
157
|
+
? systemTheme?.matches ? "dark" : "light"
|
|
158
|
+
: state.themePreference;
|
|
159
|
+
document.documentElement.dataset.theme = resolved;
|
|
160
|
+
document.documentElement.style.colorScheme = resolved;
|
|
161
|
+
}
|
|
162
|
+
systemTheme?.addEventListener?.("change", () => {
|
|
163
|
+
if (state.themePreference === "system") applyThemePreference("system");
|
|
164
|
+
});
|
|
129
165
|
const tokenStorageKey = "openpi.web.token";
|
|
166
|
+
const DEFAULT_API_TIMEOUT_MS = 15_000;
|
|
167
|
+
const PROMPT_ADMISSION_TIMEOUT_MS = 30_000;
|
|
168
|
+
const SSE_STALE_TIMEOUT_MS = 45_000;
|
|
169
|
+
const HEARTBEATS_PER_SNAPSHOT = 4;
|
|
130
170
|
const fragmentToken = new URLSearchParams(location.hash.slice(1)).get("token");
|
|
131
171
|
let token = fragmentToken;
|
|
132
172
|
if (fragmentToken) {
|
|
@@ -139,6 +179,21 @@ const headers = (json = false) => ({
|
|
|
139
179
|
Authorization: `Bearer ${token}`,
|
|
140
180
|
...(json ? { "Content-Type": "application/json" } : {}),
|
|
141
181
|
});
|
|
182
|
+
|
|
183
|
+
function setComposerFeedback(message, kind = "status") {
|
|
184
|
+
state.composerFeedback = message ? { message, kind } : null;
|
|
185
|
+
const hint = $("composer-hint");
|
|
186
|
+
if (!hint) return;
|
|
187
|
+
hint.textContent = message || "";
|
|
188
|
+
for (const candidate of ["status", "error", "connection"]) {
|
|
189
|
+
hint.classList.toggle(candidate, candidate === kind && Boolean(message));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function clearComposerFeedback(kind) {
|
|
194
|
+
if (kind && state.composerFeedback?.kind !== kind) return;
|
|
195
|
+
setComposerFeedback("");
|
|
196
|
+
}
|
|
142
197
|
const escapeHtml = (value) =>
|
|
143
198
|
String(value ?? "").replace(
|
|
144
199
|
/[&<>"']/g,
|
|
@@ -196,13 +251,45 @@ function renderMarkdown(value) {
|
|
|
196
251
|
}
|
|
197
252
|
|
|
198
253
|
async function api(path, options = {}) {
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
254
|
+
const {
|
|
255
|
+
timeoutMs = DEFAULT_API_TIMEOUT_MS,
|
|
256
|
+
timeoutMessage = t("requestTimeout"),
|
|
257
|
+
...requestOptions
|
|
258
|
+
} = options;
|
|
259
|
+
const controller = new AbortController();
|
|
260
|
+
let timedOut = false;
|
|
261
|
+
const timer = window.setTimeout(() => {
|
|
262
|
+
timedOut = true;
|
|
263
|
+
controller.abort();
|
|
264
|
+
}, timeoutMs);
|
|
265
|
+
try {
|
|
266
|
+
const response = await fetch(path, {
|
|
267
|
+
...requestOptions,
|
|
268
|
+
signal: controller.signal,
|
|
269
|
+
headers: {
|
|
270
|
+
...headers(Boolean(requestOptions.body)),
|
|
271
|
+
...requestOptions.headers,
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
const body = await response.json().catch(() => ({}));
|
|
275
|
+
if (!response.ok) {
|
|
276
|
+
const failure = new Error(body.error || `Request failed (${response.status})`);
|
|
277
|
+
failure.name = "WebApiResponseError";
|
|
278
|
+
if (typeof body.code === "string") failure.code = body.code;
|
|
279
|
+
failure.status = response.status;
|
|
280
|
+
throw failure;
|
|
281
|
+
}
|
|
282
|
+
return body;
|
|
283
|
+
} catch (error) {
|
|
284
|
+
if (timedOut) {
|
|
285
|
+
const timeout = new Error(timeoutMessage);
|
|
286
|
+
timeout.name = "WebRequestTimeout";
|
|
287
|
+
throw timeout;
|
|
288
|
+
}
|
|
289
|
+
throw error;
|
|
290
|
+
} finally {
|
|
291
|
+
window.clearTimeout(timer);
|
|
292
|
+
}
|
|
206
293
|
}
|
|
207
294
|
|
|
208
295
|
function sessionTitle(session) {
|
|
@@ -214,6 +301,18 @@ function compactSummary(value, limit = 96) {
|
|
|
214
301
|
return text.length > limit ? `${text.slice(0, limit - 1)}…` : text;
|
|
215
302
|
}
|
|
216
303
|
|
|
304
|
+
function applyPromptAcceptedState(alreadySettled) {
|
|
305
|
+
if (alreadySettled) {
|
|
306
|
+
if (state.livePhase !== "running") {
|
|
307
|
+
state.liveRunning = false;
|
|
308
|
+
state.livePhase = "idle";
|
|
309
|
+
}
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
state.liveRunning = true;
|
|
313
|
+
if (state.livePhase !== "running") state.livePhase = "preparing";
|
|
314
|
+
}
|
|
315
|
+
|
|
217
316
|
function relativeTime(value) {
|
|
218
317
|
const elapsed = Date.now() - new Date(value).getTime();
|
|
219
318
|
if (elapsed < 60_000) return "now";
|
|
@@ -482,6 +581,13 @@ function updateComposer() {
|
|
|
482
581
|
!selected &&
|
|
483
582
|
!state.snapshot?.currentSessionId;
|
|
484
583
|
const canCompose = active || newSessionDraft;
|
|
584
|
+
const activeTurn = active
|
|
585
|
+
? state.activeTurn || state.snapshot?.runtime.activeTurn || null
|
|
586
|
+
: null;
|
|
587
|
+
const canStop = Boolean(
|
|
588
|
+
activeTurn &&
|
|
589
|
+
(state.snapshot?.runtime.status === "running" || state.liveRunning),
|
|
590
|
+
);
|
|
485
591
|
$("prompt-input").disabled =
|
|
486
592
|
state.sessionSwitching || (!canCompose && Boolean(state.selectedWorkspace));
|
|
487
593
|
$("send-prompt").disabled =
|
|
@@ -489,6 +595,9 @@ function updateComposer() {
|
|
|
489
595
|
!canCompose ||
|
|
490
596
|
!state.selectedWorkspace ||
|
|
491
597
|
state.promptAdmissionPending;
|
|
598
|
+
$("send-prompt").hidden = canStop;
|
|
599
|
+
$("stop-turn").hidden = !canStop;
|
|
600
|
+
$("stop-turn").disabled = state.turnCancellationPending;
|
|
492
601
|
const modelPicker = $("model-picker");
|
|
493
602
|
const modelPickerValue = $("model-picker-value");
|
|
494
603
|
const modelMenu = $("model-menu");
|
|
@@ -527,11 +636,30 @@ function updateComposer() {
|
|
|
527
636
|
: active
|
|
528
637
|
? t("promptMessage")
|
|
529
638
|
: t("promptReadonly");
|
|
530
|
-
|
|
639
|
+
const defaultHint = canCompose
|
|
531
640
|
? state.snapshot.runtime.status === "running" || state.liveRunning
|
|
532
641
|
? t("queuedHint")
|
|
533
642
|
: t("enterHint")
|
|
534
643
|
: t("activeOnlyHint");
|
|
644
|
+
const feedback = state.composerFeedback;
|
|
645
|
+
const hint = $("composer-hint");
|
|
646
|
+
const receipt = state.pendingFollowUpsReceipt;
|
|
647
|
+
hint.textContent = state.turnCancellationPending
|
|
648
|
+
? t("stoppingTurn")
|
|
649
|
+
: state.turnTerminalStatus === "cancelled"
|
|
650
|
+
? t("stoppedTurn")
|
|
651
|
+
: receipt !== null
|
|
652
|
+
? receipt > 0
|
|
653
|
+
? t("pendingFollowUpsHint").replace("{count}", String(receipt))
|
|
654
|
+
: t("acceptedHint")
|
|
655
|
+
: feedback?.message || defaultHint;
|
|
656
|
+
hint.classList.toggle("receipt", receipt > 0);
|
|
657
|
+
for (const candidate of ["status", "error", "connection"]) {
|
|
658
|
+
hint.classList.toggle(
|
|
659
|
+
candidate,
|
|
660
|
+
receipt === null && feedback?.kind === candidate,
|
|
661
|
+
);
|
|
662
|
+
}
|
|
535
663
|
}
|
|
536
664
|
|
|
537
665
|
async function selectModel(value) {
|
|
@@ -605,15 +733,19 @@ async function refreshSnapshot({
|
|
|
605
733
|
return false;
|
|
606
734
|
}
|
|
607
735
|
state.snapshot = snapshot;
|
|
736
|
+
if (resetCursor) resetLiveState();
|
|
737
|
+
state.activeTurn = snapshot.runtime.activeTurn || null;
|
|
738
|
+
if (snapshot.runtime.status === "running") state.liveRunning = true;
|
|
739
|
+
applyThemePreference(snapshot.preferences?.theme);
|
|
608
740
|
if (
|
|
609
741
|
state.snapshot.runtime.status !== "running" &&
|
|
610
|
-
!state.promptAdmissionPending
|
|
742
|
+
!state.promptAdmissionPending &&
|
|
743
|
+
!state.promptAdmission
|
|
611
744
|
) {
|
|
612
745
|
state.liveRunning = false;
|
|
613
746
|
state.livePhase = "idle";
|
|
614
747
|
state.liveRetry = null;
|
|
615
748
|
}
|
|
616
|
-
if (resetCursor) resetLiveState();
|
|
617
749
|
state.cursor = resetCursor || state.cursor === null
|
|
618
750
|
? state.snapshot.cursor
|
|
619
751
|
: Math.max(state.cursor, state.snapshot.cursor);
|
|
@@ -640,8 +772,7 @@ async function refreshSnapshot({
|
|
|
640
772
|
) return false;
|
|
641
773
|
$("connection-state").textContent = "Unavailable";
|
|
642
774
|
$("connection-state").classList.add("reconnecting");
|
|
643
|
-
|
|
644
|
-
$("composer-hint").classList.add("error");
|
|
775
|
+
setComposerFeedback(error.message, "error");
|
|
645
776
|
return false;
|
|
646
777
|
}
|
|
647
778
|
}
|
|
@@ -654,6 +785,7 @@ async function selectSession(path) {
|
|
|
654
785
|
state.selectedPath = path;
|
|
655
786
|
state.promptAdmissionPending = false;
|
|
656
787
|
state.promptAdmissionToken = null;
|
|
788
|
+
state.promptAdmission = null;
|
|
657
789
|
resetLiveState();
|
|
658
790
|
document.body.classList.remove("sidebar-open");
|
|
659
791
|
renderWorkspaces();
|
|
@@ -718,11 +850,14 @@ async function sendPrompt() {
|
|
|
718
850
|
await chooseWorkspace();
|
|
719
851
|
}
|
|
720
852
|
const content = $("prompt-input").value.trim();
|
|
853
|
+
if (state.promptAdmissionPending) {
|
|
854
|
+
setComposerFeedback(t("admissionPendingHint"));
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
721
857
|
if (
|
|
722
858
|
!content ||
|
|
723
859
|
!state.selectedWorkspace ||
|
|
724
|
-
state.sessionSwitching
|
|
725
|
-
state.promptAdmissionPending
|
|
860
|
+
state.sessionSwitching
|
|
726
861
|
) return;
|
|
727
862
|
if (!state.snapshot?.selectedSession?.id) {
|
|
728
863
|
await createSession(state.selectedWorkspace);
|
|
@@ -731,38 +866,79 @@ async function sendPrompt() {
|
|
|
731
866
|
if (!sessionId || state.sessionSwitching || state.promptAdmissionPending) return;
|
|
732
867
|
const epoch = state.sessionEpoch;
|
|
733
868
|
const admissionToken = ++state.promptAdmissionSequence;
|
|
734
|
-
const
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
869
|
+
const retrying =
|
|
870
|
+
state.promptAdmission?.sessionId === sessionId &&
|
|
871
|
+
state.promptAdmission?.content === content;
|
|
872
|
+
const commandId = retrying
|
|
873
|
+
? state.promptAdmission.commandId
|
|
874
|
+
: globalThis.crypto?.randomUUID?.() ||
|
|
875
|
+
`web-prompt-${Date.now()}-${admissionToken}`;
|
|
876
|
+
const optimisticKey = retrying
|
|
877
|
+
? state.promptAdmission.optimisticKey
|
|
878
|
+
: `optimistic-${commandId}`;
|
|
879
|
+
if (!retrying) {
|
|
880
|
+
state.liveMessages = [
|
|
881
|
+
...state.liveMessages,
|
|
882
|
+
{ key: optimisticKey, message: { role: "user", content } },
|
|
883
|
+
].slice(-8);
|
|
884
|
+
}
|
|
885
|
+
// Keep this attempt before dispatch. A timeout may be a lost receipt, not a
|
|
886
|
+
// failed admission, so the next submit must replay this exact request.
|
|
887
|
+
state.promptAdmission = { sessionId, content, commandId, optimisticKey };
|
|
739
888
|
state.promptAdmissionPending = true;
|
|
740
889
|
state.promptAdmissionToken = admissionToken;
|
|
890
|
+
clearComposerFeedback();
|
|
891
|
+
state.pendingFollowUpsReceipt = null;
|
|
892
|
+
state.turnTerminalStatus = null;
|
|
741
893
|
renderConversation();
|
|
742
|
-
$("composer-hint").classList.remove("error");
|
|
743
894
|
try {
|
|
744
895
|
const receipt = await api("/api/prompt", {
|
|
745
896
|
method: "POST",
|
|
746
|
-
body: JSON.stringify({ sessionId, content }),
|
|
897
|
+
body: JSON.stringify({ sessionId, content, commandId, retry: retrying }),
|
|
898
|
+
timeoutMs: PROMPT_ADMISSION_TIMEOUT_MS,
|
|
899
|
+
timeoutMessage: t("admissionTimeout"),
|
|
747
900
|
});
|
|
748
901
|
if (epoch !== state.sessionEpoch || state.promptAdmissionToken !== admissionToken) return;
|
|
749
902
|
const alreadySettled = state.terminalPromptIds.has(receipt.id);
|
|
750
|
-
|
|
751
|
-
state.
|
|
903
|
+
applyPromptAcceptedState(alreadySettled);
|
|
904
|
+
if (state.promptAdmission?.commandId === commandId) {
|
|
905
|
+
state.promptAdmission = null;
|
|
906
|
+
}
|
|
907
|
+
state.pendingFollowUpsReceipt = receipt.pendingFollowUps;
|
|
752
908
|
$("prompt-input").value = "";
|
|
753
909
|
resizePrompt();
|
|
754
|
-
|
|
910
|
+
clearComposerFeedback();
|
|
755
911
|
scheduleSnapshotRefresh(120);
|
|
756
912
|
} catch (error) {
|
|
757
913
|
if (epoch !== state.sessionEpoch || state.promptAdmissionToken !== admissionToken) return;
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
914
|
+
const knownRejection =
|
|
915
|
+
error?.name === "WebApiResponseError" &&
|
|
916
|
+
[
|
|
917
|
+
"WORKSPACE_REQUIRED",
|
|
918
|
+
"SESSION_CONFLICT",
|
|
919
|
+
"PROMPT_REJECTED",
|
|
920
|
+
"COMMAND_CONFLICT",
|
|
921
|
+
"PROMPT_ADMISSION_CAPACITY",
|
|
922
|
+
].includes(error?.code);
|
|
923
|
+
if (!knownRejection) {
|
|
924
|
+
if (state.livePhase !== "running") {
|
|
925
|
+
state.liveRunning = true;
|
|
926
|
+
state.livePhase = "preparing";
|
|
927
|
+
}
|
|
928
|
+
state.liveRetry = null;
|
|
929
|
+
setComposerFeedback(
|
|
930
|
+
error?.name === "WebRequestTimeout" ? t("admissionTimeout") : error.message,
|
|
931
|
+
"connection",
|
|
932
|
+
);
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
if (state.promptAdmission?.commandId === commandId) {
|
|
936
|
+
state.promptAdmission = null;
|
|
937
|
+
}
|
|
761
938
|
state.liveMessages = state.liveMessages.filter(
|
|
762
939
|
(entry) => entry.key !== optimisticKey,
|
|
763
940
|
);
|
|
764
|
-
|
|
765
|
-
$("composer-hint").classList.add("error");
|
|
941
|
+
setComposerFeedback(error.message, "error");
|
|
766
942
|
} finally {
|
|
767
943
|
if (epoch === state.sessionEpoch && state.promptAdmissionToken === admissionToken) {
|
|
768
944
|
state.promptAdmissionPending = false;
|
|
@@ -772,6 +948,36 @@ async function sendPrompt() {
|
|
|
772
948
|
}
|
|
773
949
|
}
|
|
774
950
|
|
|
951
|
+
async function cancelActiveTurn() {
|
|
952
|
+
const turn = state.activeTurn || state.snapshot?.runtime.activeTurn;
|
|
953
|
+
if (!turn || state.turnCancellationPending || state.sessionSwitching) return;
|
|
954
|
+
const epoch = state.sessionEpoch;
|
|
955
|
+
state.turnCancellationPending = true;
|
|
956
|
+
$("composer-hint").classList.remove("error");
|
|
957
|
+
$("composer-hint").textContent = t("stoppingTurn");
|
|
958
|
+
updateComposer();
|
|
959
|
+
try {
|
|
960
|
+
const receipt = await api("/api/turns/cancel", {
|
|
961
|
+
method: "POST",
|
|
962
|
+
body: JSON.stringify(turn),
|
|
963
|
+
});
|
|
964
|
+
if (epoch !== state.sessionEpoch) return;
|
|
965
|
+
if (receipt.state === "accepted" || receipt.state === "already-settled") {
|
|
966
|
+
$("composer-hint").textContent = t("stoppedTurn");
|
|
967
|
+
}
|
|
968
|
+
} catch (error) {
|
|
969
|
+
if (epoch !== state.sessionEpoch) return;
|
|
970
|
+
const message = error.message;
|
|
971
|
+
await refreshSnapshot({ epoch });
|
|
972
|
+
if (epoch === state.sessionEpoch) showNotice(message);
|
|
973
|
+
} finally {
|
|
974
|
+
if (epoch === state.sessionEpoch) {
|
|
975
|
+
state.turnCancellationPending = false;
|
|
976
|
+
renderConversation();
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
|
|
775
981
|
function resizePrompt() {
|
|
776
982
|
const input = $("prompt-input");
|
|
777
983
|
const maxHeight = 220;
|
|
@@ -853,6 +1059,7 @@ async function createSession(workspacePath) {
|
|
|
853
1059
|
state.selectedPath = null;
|
|
854
1060
|
state.promptAdmissionPending = false;
|
|
855
1061
|
state.promptAdmissionToken = null;
|
|
1062
|
+
state.promptAdmission = null;
|
|
856
1063
|
resetLiveState();
|
|
857
1064
|
document.body.classList.remove("sidebar-open");
|
|
858
1065
|
renderWorkspaces();
|
|
@@ -897,8 +1104,7 @@ async function createSession(workspacePath) {
|
|
|
897
1104
|
}
|
|
898
1105
|
|
|
899
1106
|
function showNotice(message) {
|
|
900
|
-
|
|
901
|
-
$("composer-hint").classList.add("error");
|
|
1107
|
+
setComposerFeedback(message, "error");
|
|
902
1108
|
}
|
|
903
1109
|
|
|
904
1110
|
function openWorkspaceMenu(path, anchor) {
|
|
@@ -1066,21 +1272,59 @@ function applyRuntimeEvent(event) {
|
|
|
1066
1272
|
scheduleSnapshotRefresh();
|
|
1067
1273
|
} else if (event.type === "prompt_accepted") {
|
|
1068
1274
|
const alreadySettled = state.terminalPromptIds.has(event.detail?.commandId);
|
|
1069
|
-
|
|
1070
|
-
state.
|
|
1275
|
+
applyPromptAcceptedState(alreadySettled);
|
|
1276
|
+
state.pendingFollowUpsReceipt = Number.isInteger(event.detail?.pendingFollowUps)
|
|
1277
|
+
? event.detail.pendingFollowUps
|
|
1278
|
+
: state.pendingFollowUpsReceipt;
|
|
1071
1279
|
state.liveRetry = null;
|
|
1072
1280
|
renderConversation();
|
|
1073
|
-
} else if (event.type === "
|
|
1281
|
+
} else if (event.type === "turn_started") {
|
|
1282
|
+
state.activeTurn = {
|
|
1283
|
+
sessionId: event.detail?.sessionId,
|
|
1284
|
+
commandId: event.detail?.commandId,
|
|
1285
|
+
epoch: event.detail?.epoch,
|
|
1286
|
+
};
|
|
1074
1287
|
state.liveRunning = true;
|
|
1288
|
+
state.turnTerminalStatus = null;
|
|
1075
1289
|
state.livePhase = "running";
|
|
1076
1290
|
state.liveRetry = null;
|
|
1077
1291
|
renderConversation();
|
|
1078
|
-
} else if (event.type === "
|
|
1079
|
-
if (event.
|
|
1080
|
-
state.liveRunning =
|
|
1081
|
-
state.livePhase = "
|
|
1292
|
+
} else if (event.type === "agent_start") {
|
|
1293
|
+
if (event.detail?.activeTurn) state.activeTurn = event.detail.activeTurn;
|
|
1294
|
+
state.liveRunning = true;
|
|
1295
|
+
state.livePhase = "running";
|
|
1082
1296
|
state.liveRetry = null;
|
|
1083
1297
|
renderConversation();
|
|
1298
|
+
} else if (event.type === "turn_settled") {
|
|
1299
|
+
rememberTerminalPrompt(event.detail?.commandId);
|
|
1300
|
+
const isActiveTurn =
|
|
1301
|
+
state.activeTurn?.sessionId === event.detail?.sessionId &&
|
|
1302
|
+
state.activeTurn?.commandId === event.detail?.commandId &&
|
|
1303
|
+
state.activeTurn?.epoch === event.detail?.epoch;
|
|
1304
|
+
if (isActiveTurn) {
|
|
1305
|
+
state.activeTurn = null;
|
|
1306
|
+
state.liveRunning = false;
|
|
1307
|
+
state.livePhase = "idle";
|
|
1308
|
+
state.liveRetry = null;
|
|
1309
|
+
state.turnTerminalStatus = event.detail?.outcome || null;
|
|
1310
|
+
}
|
|
1311
|
+
renderConversation();
|
|
1312
|
+
} else if (event.type === "agent_settled") {
|
|
1313
|
+
state.pendingFollowUpsReceipt = null;
|
|
1314
|
+
if (!state.activeTurn) {
|
|
1315
|
+
state.liveRunning = false;
|
|
1316
|
+
state.livePhase = "idle";
|
|
1317
|
+
state.liveRetry = null;
|
|
1318
|
+
}
|
|
1319
|
+
renderConversation();
|
|
1320
|
+
} else if (event.type === "prompt_settled") {
|
|
1321
|
+
rememberTerminalPrompt(event.detail?.commandId);
|
|
1322
|
+
if (state.livePhase !== "running") {
|
|
1323
|
+
state.liveRunning = false;
|
|
1324
|
+
state.livePhase = "idle";
|
|
1325
|
+
state.liveRetry = null;
|
|
1326
|
+
}
|
|
1327
|
+
renderConversation();
|
|
1084
1328
|
} else if (event.detail?.message) {
|
|
1085
1329
|
if (event.detail.message.role === "user") {
|
|
1086
1330
|
state.liveMessages = state.liveMessages.filter(
|
|
@@ -1100,6 +1344,8 @@ function applyRuntimeEvent(event) {
|
|
|
1100
1344
|
[
|
|
1101
1345
|
"agent_start",
|
|
1102
1346
|
"agent_settled",
|
|
1347
|
+
"turn_started",
|
|
1348
|
+
"turn_settled",
|
|
1103
1349
|
"prompt_settled",
|
|
1104
1350
|
"message_end",
|
|
1105
1351
|
"tool_execution_end",
|
|
@@ -1160,18 +1406,33 @@ function rememberCompletedActivation(commandId) {
|
|
|
1160
1406
|
}
|
|
1161
1407
|
|
|
1162
1408
|
let eventLoopStarted = false;
|
|
1409
|
+
let eventLoopStopped = false;
|
|
1410
|
+
let activeEventReader = null;
|
|
1411
|
+
|
|
1412
|
+
window.addEventListener("pagehide", (event) => {
|
|
1413
|
+
// A bfcache entry resumes this same document and its event loop. Do not
|
|
1414
|
+
// create a second SSE connection on pageshow.
|
|
1415
|
+
if (event.persisted) return;
|
|
1416
|
+
eventLoopStopped = true;
|
|
1417
|
+
void activeEventReader?.cancel().catch(() => undefined);
|
|
1418
|
+
});
|
|
1419
|
+
|
|
1163
1420
|
function resetLiveState() {
|
|
1164
1421
|
state.liveMessages = [];
|
|
1165
1422
|
state.liveRunning = false;
|
|
1423
|
+
state.activeTurn = null;
|
|
1424
|
+
state.turnCancellationPending = false;
|
|
1425
|
+
state.turnTerminalStatus = null;
|
|
1166
1426
|
state.livePhase = "idle";
|
|
1167
1427
|
state.liveRetry = null;
|
|
1428
|
+
state.pendingFollowUpsReceipt = null;
|
|
1168
1429
|
}
|
|
1169
1430
|
|
|
1170
1431
|
async function connectEvents() {
|
|
1171
1432
|
if (eventLoopStarted) return;
|
|
1172
1433
|
eventLoopStarted = true;
|
|
1173
1434
|
let reconnectDelay = 500;
|
|
1174
|
-
while (
|
|
1435
|
+
while (!eventLoopStopped) {
|
|
1175
1436
|
let reader = null;
|
|
1176
1437
|
let recoveryAttempted = false;
|
|
1177
1438
|
try {
|
|
@@ -1193,17 +1454,28 @@ async function connectEvents() {
|
|
|
1193
1454
|
if (!response.ok || !response.body) throw new Error("event connection failed");
|
|
1194
1455
|
$("connection-state").textContent = "Connected";
|
|
1195
1456
|
$("connection-state").classList.remove("reconnecting");
|
|
1457
|
+
clearComposerFeedback("connection");
|
|
1196
1458
|
reconnectDelay = 500;
|
|
1197
1459
|
reader = response.body.getReader();
|
|
1460
|
+
activeEventReader = reader;
|
|
1198
1461
|
const decoder = new TextDecoder();
|
|
1199
1462
|
let buffer = "";
|
|
1463
|
+
let heartbeatCount = 0;
|
|
1200
1464
|
while (true) {
|
|
1201
|
-
const { done, value } = await reader
|
|
1465
|
+
const { done, value } = await readEventChunk(reader);
|
|
1202
1466
|
if (done) throw new Error("event connection closed");
|
|
1203
1467
|
buffer += decoder.decode(value, { stream: true });
|
|
1204
1468
|
const records = buffer.split("\n\n");
|
|
1205
1469
|
buffer = records.pop() || "";
|
|
1206
1470
|
for (const record of records) {
|
|
1471
|
+
if (record.split("\n").some((item) => item === ": heartbeat")) {
|
|
1472
|
+
heartbeatCount++;
|
|
1473
|
+
if (heartbeatCount >= HEARTBEATS_PER_SNAPSHOT) {
|
|
1474
|
+
heartbeatCount = 0;
|
|
1475
|
+
scheduleSnapshotRefresh(0);
|
|
1476
|
+
}
|
|
1477
|
+
continue;
|
|
1478
|
+
}
|
|
1207
1479
|
const line = record.split("\n").find((item) => item.startsWith("data: "));
|
|
1208
1480
|
if (!line) continue;
|
|
1209
1481
|
const event = JSON.parse(line.slice(6));
|
|
@@ -1217,21 +1489,42 @@ async function connectEvents() {
|
|
|
1217
1489
|
}
|
|
1218
1490
|
} catch {
|
|
1219
1491
|
await reader?.cancel().catch(() => undefined);
|
|
1492
|
+
if (activeEventReader === reader) activeEventReader = null;
|
|
1220
1493
|
reader = null;
|
|
1494
|
+
if (eventLoopStopped) break;
|
|
1221
1495
|
$("connection-state").textContent = "Reconnecting";
|
|
1222
1496
|
$("connection-state").classList.add("reconnecting");
|
|
1223
1497
|
const recovered = recoveryAttempted
|
|
1224
1498
|
? false
|
|
1225
1499
|
: await refreshSnapshot({ resetCursor: true });
|
|
1226
1500
|
if (!recovered) resetLiveState();
|
|
1501
|
+
setComposerFeedback(t("reconnectingHint"), "connection");
|
|
1227
1502
|
await new Promise((resolve) => setTimeout(resolve, reconnectDelay));
|
|
1228
1503
|
reconnectDelay = Math.min(reconnectDelay * 2, 5_000);
|
|
1229
1504
|
} finally {
|
|
1230
1505
|
await reader?.cancel().catch(() => undefined);
|
|
1506
|
+
if (activeEventReader === reader) activeEventReader = null;
|
|
1231
1507
|
}
|
|
1232
1508
|
}
|
|
1233
1509
|
}
|
|
1234
1510
|
|
|
1511
|
+
async function readEventChunk(reader, timeoutMs = SSE_STALE_TIMEOUT_MS) {
|
|
1512
|
+
let timer;
|
|
1513
|
+
try {
|
|
1514
|
+
return await Promise.race([
|
|
1515
|
+
reader.read(),
|
|
1516
|
+
new Promise((_, reject) => {
|
|
1517
|
+
timer = window.setTimeout(
|
|
1518
|
+
() => reject(new Error("event stream stalled")),
|
|
1519
|
+
timeoutMs,
|
|
1520
|
+
);
|
|
1521
|
+
}),
|
|
1522
|
+
]);
|
|
1523
|
+
} finally {
|
|
1524
|
+
if (timer !== undefined) window.clearTimeout(timer);
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1235
1528
|
const collapseButton = $("collapse-sidebar");
|
|
1236
1529
|
const collapsedStorageKey = "openpi.sidebar-collapsed";
|
|
1237
1530
|
const setSidebarCollapsed = (collapsed) => {
|
|
@@ -1384,7 +1677,15 @@ $("composer")?.addEventListener("submit", (event) => {
|
|
|
1384
1677
|
if (state.selectedWorkspace) void sendPrompt();
|
|
1385
1678
|
else void chooseWorkspace();
|
|
1386
1679
|
});
|
|
1387
|
-
$("
|
|
1680
|
+
$("stop-turn")?.addEventListener("click", () => {
|
|
1681
|
+
void cancelActiveTurn();
|
|
1682
|
+
});
|
|
1683
|
+
$("prompt-input")?.addEventListener("input", () => {
|
|
1684
|
+
state.pendingFollowUpsReceipt = null;
|
|
1685
|
+
if (!state.promptAdmissionPending) clearComposerFeedback();
|
|
1686
|
+
resizePrompt();
|
|
1687
|
+
updateComposer();
|
|
1688
|
+
});
|
|
1388
1689
|
$("prompt-input")?.addEventListener("keydown", (event) => {
|
|
1389
1690
|
if (event.isComposing || event.keyCode === 229) return;
|
|
1390
1691
|
if (event.key === "Enter" && !event.shiftKey) {
|
|
@@ -1394,5 +1695,6 @@ $("prompt-input")?.addEventListener("keydown", (event) => {
|
|
|
1394
1695
|
}
|
|
1395
1696
|
});
|
|
1396
1697
|
|
|
1698
|
+
applyThemePreference("system");
|
|
1397
1699
|
applyLanguage();
|
|
1398
1700
|
void connectEvents();
|
package/web/ui/index.html
CHANGED
|
@@ -118,6 +118,9 @@
|
|
|
118
118
|
<button id="send-prompt" class="send-button" type="submit" aria-label="Send" title="Send">
|
|
119
119
|
<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 19V5"></path><path d="m6 11 6-6 6 6"></path></svg>
|
|
120
120
|
</button>
|
|
121
|
+
<button id="stop-turn" class="stop-button" type="button" aria-label="Stop turn" title="Stop turn" data-i18n-aria="stopTurn" data-i18n-title="stopTurn" hidden>
|
|
122
|
+
<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><rect x="7" y="7" width="10" height="10" rx="1"></rect></svg>
|
|
123
|
+
</button>
|
|
121
124
|
</div>
|
|
122
125
|
<div id="composer-hint" class="composer-hint" aria-live="polite"></div>
|
|
123
126
|
</form>
|