viveworker 0.1.1 → 0.1.2
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/LICENSE +21 -0
- package/README.md +5 -0
- package/package.json +16 -6
- package/scripts/viveworker-bridge.mjs +154 -6
- package/scripts/viveworker.mjs +79 -3
- package/web/app.css +84 -0
- package/web/app.js +210 -11
- package/web/i18n.js +30 -0
- package/web/sw.js +1 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yuta Hoshino
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
1
3
|
# viveworker
|
|
2
4
|
|
|
5
|
+
[](https://badge.fury.io/js/viveworker)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
3
8
|
`viveworker` brings Codex Desktop to your iPhone.
|
|
4
9
|
|
|
5
10
|
When Codex needs an approval, asks whether to implement a plan, wants you to choose from options, or finishes a task while you are away from your desk, `viveworker` keeps all of that within reach on your phone. Instead of breaking your rhythm, it helps you keep vivecoding going from anywhere in your home or office.
|
package/package.json
CHANGED
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "viveworker",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
5
|
-
"author": "
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Local iPhone companion for Codex Desktop approvals, plan checks, questions, and notifications on your LAN.",
|
|
5
|
+
"author": "Yuta Hoshino <hoshino.lireneo@gmail.com>",
|
|
6
|
+
"license": "MIT",
|
|
6
7
|
"keywords": [
|
|
7
|
-
"vivecoding",
|
|
8
8
|
"codex",
|
|
9
|
-
"
|
|
9
|
+
"codex-desktop",
|
|
10
|
+
"macos",
|
|
11
|
+
"iphone",
|
|
12
|
+
"ios",
|
|
13
|
+
"pwa",
|
|
14
|
+
"web-push",
|
|
15
|
+
"notifications",
|
|
16
|
+
"approvals",
|
|
17
|
+
"lan",
|
|
18
|
+
"companion-app",
|
|
19
|
+
"vivecoding"
|
|
10
20
|
],
|
|
11
21
|
"homepage": "https://lp.hazbase.com/",
|
|
12
|
-
"repository": "viveworker/viveworker",
|
|
22
|
+
"repository": "viveworker-dev/viveworker",
|
|
13
23
|
"type": "module",
|
|
14
24
|
"bin": {
|
|
15
25
|
"viveworker": "./scripts/viveworker.mjs"
|
|
@@ -31,6 +31,9 @@ const DEFAULT_DEVICE_TRUST_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
|
31
31
|
const MAX_PAIRED_DEVICES = 200;
|
|
32
32
|
const PAIRING_RATE_LIMIT_WINDOW_MS = 15 * 60 * 1000;
|
|
33
33
|
const PAIRING_RATE_LIMIT_MAX_ATTEMPTS = 8;
|
|
34
|
+
const DEFAULT_COMPLETION_REPLY_IMAGE_MAX_BYTES = 15 * 1024 * 1024;
|
|
35
|
+
const DEFAULT_COMPLETION_REPLY_UPLOAD_TTL_MS = 24 * 60 * 60 * 1000;
|
|
36
|
+
const MAX_COMPLETION_REPLY_IMAGE_COUNT = 1;
|
|
34
37
|
|
|
35
38
|
const cli = parseCliArgs(process.argv.slice(2));
|
|
36
39
|
const envFile = resolveEnvFile(cli.envFile);
|
|
@@ -5437,8 +5440,25 @@ async function submitGenericUserInputDecision({ config, runtime, state, userInpu
|
|
|
5437
5440
|
}
|
|
5438
5441
|
}
|
|
5439
5442
|
|
|
5440
|
-
|
|
5443
|
+
function normalizeCompletionReplyLocalImagePaths(paths) {
|
|
5444
|
+
if (!Array.isArray(paths)) {
|
|
5445
|
+
return [];
|
|
5446
|
+
}
|
|
5447
|
+
return paths
|
|
5448
|
+
.map((value) => resolvePath(cleanText(value || "")))
|
|
5449
|
+
.filter(Boolean);
|
|
5450
|
+
}
|
|
5451
|
+
|
|
5452
|
+
async function handleCompletionReply({
|
|
5453
|
+
runtime,
|
|
5454
|
+
completionItem,
|
|
5455
|
+
text,
|
|
5456
|
+
planMode = false,
|
|
5457
|
+
force = false,
|
|
5458
|
+
localImagePaths = [],
|
|
5459
|
+
}) {
|
|
5441
5460
|
const messageText = cleanText(text ?? "");
|
|
5461
|
+
const normalizedLocalImagePaths = normalizeCompletionReplyLocalImagePaths(localImagePaths);
|
|
5442
5462
|
if (!messageText) {
|
|
5443
5463
|
throw new Error("completion-reply-empty");
|
|
5444
5464
|
}
|
|
@@ -5472,6 +5492,10 @@ async function handleCompletionReply({ runtime, completionItem, text, planMode =
|
|
|
5472
5492
|
const turnStartParams = {
|
|
5473
5493
|
input: buildTextInput(messageText),
|
|
5474
5494
|
attachments: [],
|
|
5495
|
+
localImagePaths: normalizedLocalImagePaths,
|
|
5496
|
+
local_image_paths: normalizedLocalImagePaths,
|
|
5497
|
+
remoteImageUrls: [],
|
|
5498
|
+
remote_image_urls: [],
|
|
5475
5499
|
cwd: null,
|
|
5476
5500
|
approvalPolicy: null,
|
|
5477
5501
|
sandboxPolicy: null,
|
|
@@ -6112,22 +6136,36 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
6112
6136
|
}
|
|
6113
6137
|
|
|
6114
6138
|
try {
|
|
6115
|
-
const
|
|
6139
|
+
const contentType = String(req.headers["content-type"] || "");
|
|
6140
|
+
const payload = contentType.includes("multipart/form-data")
|
|
6141
|
+
? await stageCompletionReplyImages(config, req)
|
|
6142
|
+
: await parseJsonBody(req);
|
|
6116
6143
|
await handleCompletionReply({
|
|
6117
6144
|
runtime,
|
|
6118
6145
|
completionItem,
|
|
6119
6146
|
text: payload?.text ?? "",
|
|
6120
6147
|
planMode: payload?.planMode === true,
|
|
6121
6148
|
force: payload?.force === true,
|
|
6149
|
+
localImagePaths: Array.isArray(payload?.localImagePaths) ? payload.localImagePaths : [],
|
|
6122
6150
|
});
|
|
6123
6151
|
return writeJson(res, 200, {
|
|
6124
6152
|
ok: true,
|
|
6125
6153
|
planMode: payload?.planMode === true,
|
|
6154
|
+
imageCount: Array.isArray(payload?.localImagePaths) ? payload.localImagePaths.length : 0,
|
|
6126
6155
|
});
|
|
6127
6156
|
} catch (error) {
|
|
6128
6157
|
if (error.message === "completion-reply-empty") {
|
|
6129
6158
|
return writeJson(res, 400, { error: error.message });
|
|
6130
6159
|
}
|
|
6160
|
+
if (
|
|
6161
|
+
error.message === "completion-reply-image-limit" ||
|
|
6162
|
+
error.message === "completion-reply-image-invalid-type" ||
|
|
6163
|
+
error.message === "completion-reply-image-too-large" ||
|
|
6164
|
+
error.message === "completion-reply-image-invalid-upload" ||
|
|
6165
|
+
error.message === "completion-reply-image-disabled"
|
|
6166
|
+
) {
|
|
6167
|
+
return writeJson(res, 400, { error: error.message });
|
|
6168
|
+
}
|
|
6131
6169
|
if (error.message === "completion-reply-unavailable") {
|
|
6132
6170
|
return writeJson(res, 409, { error: error.message });
|
|
6133
6171
|
}
|
|
@@ -6766,6 +6804,20 @@ async function parseFormBody(req) {
|
|
|
6766
6804
|
});
|
|
6767
6805
|
}
|
|
6768
6806
|
|
|
6807
|
+
async function parseMultipartBody(req) {
|
|
6808
|
+
const contentLength = Number(req.headers["content-length"]) || 0;
|
|
6809
|
+
if (contentLength > 32 * 1024 * 1024) {
|
|
6810
|
+
throw new Error("request-body-too-large");
|
|
6811
|
+
}
|
|
6812
|
+
const request = new Request("http://localhost/upload", {
|
|
6813
|
+
method: req.method || "POST",
|
|
6814
|
+
headers: req.headers,
|
|
6815
|
+
body: req,
|
|
6816
|
+
duplex: "half",
|
|
6817
|
+
});
|
|
6818
|
+
return request.formData();
|
|
6819
|
+
}
|
|
6820
|
+
|
|
6769
6821
|
async function parseJsonBody(req) {
|
|
6770
6822
|
return new Promise((resolve, reject) => {
|
|
6771
6823
|
let body = "";
|
|
@@ -6788,6 +6840,94 @@ async function parseJsonBody(req) {
|
|
|
6788
6840
|
});
|
|
6789
6841
|
}
|
|
6790
6842
|
|
|
6843
|
+
function guessUploadExtension(fileName, mimeType) {
|
|
6844
|
+
const explicitExtension = path.extname(cleanText(fileName || ""));
|
|
6845
|
+
if (explicitExtension) {
|
|
6846
|
+
return explicitExtension.toLowerCase();
|
|
6847
|
+
}
|
|
6848
|
+
const normalizedMimeType = cleanText(mimeType || "").toLowerCase();
|
|
6849
|
+
const knownExtensions = {
|
|
6850
|
+
"image/jpeg": ".jpg",
|
|
6851
|
+
"image/png": ".png",
|
|
6852
|
+
"image/webp": ".webp",
|
|
6853
|
+
"image/gif": ".gif",
|
|
6854
|
+
"image/heic": ".heic",
|
|
6855
|
+
"image/heif": ".heif",
|
|
6856
|
+
};
|
|
6857
|
+
return knownExtensions[normalizedMimeType] || ".img";
|
|
6858
|
+
}
|
|
6859
|
+
|
|
6860
|
+
async function cleanupExpiredCompletionReplyUploads(config) {
|
|
6861
|
+
try {
|
|
6862
|
+
const entries = await fs.readdir(config.replyUploadsDir, { withFileTypes: true });
|
|
6863
|
+
const cutoffMs = Date.now() - config.completionReplyUploadTtlMs;
|
|
6864
|
+
await Promise.all(entries.map(async (entry) => {
|
|
6865
|
+
if (!entry.isFile()) {
|
|
6866
|
+
return;
|
|
6867
|
+
}
|
|
6868
|
+
const filePath = path.join(config.replyUploadsDir, entry.name);
|
|
6869
|
+
try {
|
|
6870
|
+
const stat = await fs.stat(filePath);
|
|
6871
|
+
if (Number(stat.mtimeMs) < cutoffMs) {
|
|
6872
|
+
await fs.rm(filePath, { force: true });
|
|
6873
|
+
}
|
|
6874
|
+
} catch {
|
|
6875
|
+
// Ignore best-effort cleanup errors.
|
|
6876
|
+
}
|
|
6877
|
+
}));
|
|
6878
|
+
} catch {
|
|
6879
|
+
// Ignore missing upload dir.
|
|
6880
|
+
}
|
|
6881
|
+
}
|
|
6882
|
+
|
|
6883
|
+
async function stageCompletionReplyImages(config, req) {
|
|
6884
|
+
const formData = await parseMultipartBody(req);
|
|
6885
|
+
const files = formData
|
|
6886
|
+
.getAll("image")
|
|
6887
|
+
.filter((value) => typeof File !== "undefined" && value instanceof File);
|
|
6888
|
+
|
|
6889
|
+
if (files.length > 0) {
|
|
6890
|
+
throw new Error("completion-reply-image-disabled");
|
|
6891
|
+
}
|
|
6892
|
+
|
|
6893
|
+
if (files.length > MAX_COMPLETION_REPLY_IMAGE_COUNT) {
|
|
6894
|
+
throw new Error("completion-reply-image-limit");
|
|
6895
|
+
}
|
|
6896
|
+
|
|
6897
|
+
await cleanupExpiredCompletionReplyUploads(config);
|
|
6898
|
+
await fs.mkdir(config.replyUploadsDir, { recursive: true });
|
|
6899
|
+
|
|
6900
|
+
const localImagePaths = [];
|
|
6901
|
+
for (const file of files) {
|
|
6902
|
+
const mimeType = cleanText(file.type || "").toLowerCase();
|
|
6903
|
+
if (!mimeType.startsWith("image/")) {
|
|
6904
|
+
throw new Error("completion-reply-image-invalid-type");
|
|
6905
|
+
}
|
|
6906
|
+
if (!Number.isFinite(file.size) || file.size <= 0) {
|
|
6907
|
+
throw new Error("completion-reply-image-invalid-upload");
|
|
6908
|
+
}
|
|
6909
|
+
if (file.size > config.completionReplyImageMaxBytes) {
|
|
6910
|
+
throw new Error("completion-reply-image-too-large");
|
|
6911
|
+
}
|
|
6912
|
+
|
|
6913
|
+
const extension = guessUploadExtension(file.name, mimeType);
|
|
6914
|
+
const stagedFilePath = path.join(
|
|
6915
|
+
config.replyUploadsDir,
|
|
6916
|
+
`${Date.now()}-${crypto.randomUUID()}${extension}`
|
|
6917
|
+
);
|
|
6918
|
+
const buffer = Buffer.from(await file.arrayBuffer());
|
|
6919
|
+
await fs.writeFile(stagedFilePath, buffer, { mode: 0o600 });
|
|
6920
|
+
localImagePaths.push(stagedFilePath);
|
|
6921
|
+
}
|
|
6922
|
+
|
|
6923
|
+
return {
|
|
6924
|
+
text: cleanText(formData.get("text") ?? ""),
|
|
6925
|
+
planMode: String(formData.get("planMode") ?? "") === "true",
|
|
6926
|
+
force: String(formData.get("force") ?? "") === "true",
|
|
6927
|
+
localImagePaths,
|
|
6928
|
+
};
|
|
6929
|
+
}
|
|
6930
|
+
|
|
6791
6931
|
function isLoopbackRequest(req) {
|
|
6792
6932
|
const remoteAddress = cleanText(req.socket?.remoteAddress ?? "");
|
|
6793
6933
|
return (
|
|
@@ -7727,6 +7867,7 @@ function isLoopbackHostname(value) {
|
|
|
7727
7867
|
|
|
7728
7868
|
function buildConfig(cli) {
|
|
7729
7869
|
const codexHome = resolvePath(process.env.CODEX_HOME || path.join(os.homedir(), ".codex"));
|
|
7870
|
+
const stateFile = resolvePath(process.env.STATE_FILE || path.join(workspaceRoot, ".viveworker-state.json"));
|
|
7730
7871
|
return {
|
|
7731
7872
|
dryRun: cli.dryRun || truthy(process.env.DRY_RUN),
|
|
7732
7873
|
once: cli.once,
|
|
@@ -7740,7 +7881,8 @@ function buildConfig(cli) {
|
|
|
7740
7881
|
sessionIndexFile: resolvePath(process.env.SESSION_INDEX_FILE || path.join(codexHome, "session_index.jsonl")),
|
|
7741
7882
|
historyFile: resolvePath(process.env.HISTORY_FILE || path.join(codexHome, "history.jsonl")),
|
|
7742
7883
|
codexLogsDbFile: resolvePath(process.env.CODEX_LOGS_DB_FILE || ""),
|
|
7743
|
-
stateFile
|
|
7884
|
+
stateFile,
|
|
7885
|
+
replyUploadsDir: resolvePath(process.env.REPLY_UPLOADS_DIR || path.join(path.dirname(stateFile), "uploads")),
|
|
7744
7886
|
pollIntervalMs: numberEnv("POLL_INTERVAL_MS", 2500),
|
|
7745
7887
|
replaySeconds: numberEnv("REPLAY_SECONDS", 300),
|
|
7746
7888
|
sessionIndexRefreshMs: numberEnv("SESSION_INDEX_REFRESH_MS", 30000),
|
|
@@ -7794,6 +7936,14 @@ function buildConfig(cli) {
|
|
|
7794
7936
|
ipcReconnectMs: numberEnv("IPC_RECONNECT_MS", 1500),
|
|
7795
7937
|
ipcRequestTimeoutMs: numberEnv("IPC_REQUEST_TIMEOUT_MS", 12000),
|
|
7796
7938
|
choicePageSize: numberEnv("CHOICE_PAGE_SIZE", 5),
|
|
7939
|
+
completionReplyImageMaxBytes: numberEnv(
|
|
7940
|
+
"COMPLETION_REPLY_IMAGE_MAX_BYTES",
|
|
7941
|
+
DEFAULT_COMPLETION_REPLY_IMAGE_MAX_BYTES
|
|
7942
|
+
),
|
|
7943
|
+
completionReplyUploadTtlMs: numberEnv(
|
|
7944
|
+
"COMPLETION_REPLY_UPLOAD_TTL_MS",
|
|
7945
|
+
DEFAULT_COMPLETION_REPLY_UPLOAD_TTL_MS
|
|
7946
|
+
),
|
|
7797
7947
|
deviceTrustTtlMs: numberEnv("DEVICE_TRUST_TTL_MS", DEFAULT_DEVICE_TRUST_TTL_MS),
|
|
7798
7948
|
sessionTtlMs: numberEnv("SESSION_TTL_MS", 30 * 24 * 60 * 60 * 1000),
|
|
7799
7949
|
pairingCode: process.env.PAIRING_CODE || "",
|
|
@@ -7907,9 +8057,7 @@ function loadEnvFile(filePath) {
|
|
|
7907
8057
|
if ((value.startsWith("\"") && value.endsWith("\"")) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
7908
8058
|
value = value.slice(1, -1);
|
|
7909
8059
|
}
|
|
7910
|
-
|
|
7911
|
-
process.env[key] = value;
|
|
7912
|
-
}
|
|
8060
|
+
process.env[key] = value;
|
|
7913
8061
|
}
|
|
7914
8062
|
} catch {
|
|
7915
8063
|
// Optional env file.
|
package/scripts/viveworker.mjs
CHANGED
|
@@ -200,7 +200,14 @@ async function runSetup(cliOptions) {
|
|
|
200
200
|
|
|
201
201
|
progress.update("cli.setup.progress.health");
|
|
202
202
|
const healthy = await waitForHealth(buildLoopbackHealthUrl(publicBaseUrl));
|
|
203
|
-
|
|
203
|
+
const pairingReady = healthy
|
|
204
|
+
? await waitForExpectedPairing(publicBaseUrl, pairToken)
|
|
205
|
+
: false;
|
|
206
|
+
progress.done(healthy && pairingReady ? "cli.setup.complete" : "cli.setup.completePending");
|
|
207
|
+
if (healthy && !pairingReady) {
|
|
208
|
+
console.log("");
|
|
209
|
+
console.log(t(locale, "cli.setup.warning.stalePairingServer", { port }));
|
|
210
|
+
}
|
|
204
211
|
|
|
205
212
|
const pairPath = `/app?pairToken=${encodeURIComponent(pairToken)}`;
|
|
206
213
|
const mkcertRootCaFile = resolvePath(
|
|
@@ -309,7 +316,14 @@ async function runStart(cliOptions) {
|
|
|
309
316
|
await execCommand(["launchctl", "kickstart", "-k", `gui/${process.getuid()}/${defaultLabel}`]);
|
|
310
317
|
progress.update("cli.start.progress.health");
|
|
311
318
|
const healthy = await waitForHealth(healthUrl);
|
|
312
|
-
|
|
319
|
+
const pairingReady = healthy && rotatedPairing.rotated
|
|
320
|
+
? await waitForExpectedPairing(config.NATIVE_APPROVAL_SERVER_PUBLIC_BASE_URL || "", rotatedPairing.pairingToken)
|
|
321
|
+
: true;
|
|
322
|
+
progress.done(healthy && pairingReady ? "cli.start.launchdStarted" : "cli.start.launchdStartedPending");
|
|
323
|
+
if (healthy && !pairingReady) {
|
|
324
|
+
console.log("");
|
|
325
|
+
console.log(t(locale, "cli.setup.warning.stalePairingServer", { port: config.NATIVE_APPROVAL_SERVER_PORT || defaultServerPort }));
|
|
326
|
+
}
|
|
313
327
|
if (rotatedPairing.rotated) {
|
|
314
328
|
await printPairingInfo(locale, config);
|
|
315
329
|
}
|
|
@@ -324,7 +338,14 @@ async function runStart(cliOptions) {
|
|
|
324
338
|
});
|
|
325
339
|
progress.update("cli.start.progress.health");
|
|
326
340
|
const healthy = await waitForHealth(healthUrl);
|
|
327
|
-
|
|
341
|
+
const pairingReady = healthy && rotatedPairing.rotated
|
|
342
|
+
? await waitForExpectedPairing(config.NATIVE_APPROVAL_SERVER_PUBLIC_BASE_URL || "", rotatedPairing.pairingToken)
|
|
343
|
+
: true;
|
|
344
|
+
progress.done(healthy && pairingReady ? "cli.start.bridgeStarted" : "cli.start.bridgeStartedPending");
|
|
345
|
+
if (healthy && !pairingReady) {
|
|
346
|
+
console.log("");
|
|
347
|
+
console.log(t(locale, "cli.setup.warning.stalePairingServer", { port: config.NATIVE_APPROVAL_SERVER_PORT || defaultServerPort }));
|
|
348
|
+
}
|
|
328
349
|
if (rotatedPairing.rotated) {
|
|
329
350
|
await printPairingInfo(locale, config);
|
|
330
351
|
}
|
|
@@ -982,6 +1003,33 @@ function buildLoopbackHealthUrl(baseUrl) {
|
|
|
982
1003
|
}
|
|
983
1004
|
}
|
|
984
1005
|
|
|
1006
|
+
function buildLoopbackUrl(baseUrl, pathname, searchParams = null) {
|
|
1007
|
+
if (!baseUrl) {
|
|
1008
|
+
return "";
|
|
1009
|
+
}
|
|
1010
|
+
try {
|
|
1011
|
+
const url = new URL(baseUrl);
|
|
1012
|
+
url.hostname = "127.0.0.1";
|
|
1013
|
+
url.pathname = pathname;
|
|
1014
|
+
url.search = "";
|
|
1015
|
+
url.hash = "";
|
|
1016
|
+
if (searchParams && Object.keys(searchParams).length > 0) {
|
|
1017
|
+
const params = new URLSearchParams();
|
|
1018
|
+
for (const [key, value] of Object.entries(searchParams)) {
|
|
1019
|
+
if (value == null || value === "") {
|
|
1020
|
+
continue;
|
|
1021
|
+
}
|
|
1022
|
+
params.set(key, String(value));
|
|
1023
|
+
}
|
|
1024
|
+
const serialized = params.toString();
|
|
1025
|
+
url.search = serialized ? `?${serialized}` : "";
|
|
1026
|
+
}
|
|
1027
|
+
return url.toString();
|
|
1028
|
+
} catch {
|
|
1029
|
+
return "";
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
|
|
985
1033
|
function sleep(ms) {
|
|
986
1034
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
987
1035
|
}
|
|
@@ -1105,6 +1153,34 @@ async function waitForHealth(url, { attempts = 8, intervalMs = 500 } = {}) {
|
|
|
1105
1153
|
return false;
|
|
1106
1154
|
}
|
|
1107
1155
|
|
|
1156
|
+
async function waitForExpectedPairing(baseUrl, pairToken, { attempts = 8, intervalMs = 500 } = {}) {
|
|
1157
|
+
const token = String(pairToken || "").trim();
|
|
1158
|
+
const manifestUrl = buildLoopbackUrl(baseUrl, "/manifest.webmanifest", { pairToken: token });
|
|
1159
|
+
const expectedStartUrl = `/app?pairToken=${encodeURIComponent(token)}`;
|
|
1160
|
+
if (!token || !manifestUrl) {
|
|
1161
|
+
return false;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
1165
|
+
const result = await execCommand(buildHealthCheckArgs(manifestUrl), { ignoreError: true });
|
|
1166
|
+
if (result.ok) {
|
|
1167
|
+
try {
|
|
1168
|
+
const payload = JSON.parse(result.stdout);
|
|
1169
|
+
if (String(payload?.start_url || "").trim() === expectedStartUrl) {
|
|
1170
|
+
return true;
|
|
1171
|
+
}
|
|
1172
|
+
} catch {
|
|
1173
|
+
// Keep retrying while the new bridge instance comes up.
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
if (attempt < attempts - 1) {
|
|
1177
|
+
await sleep(intervalMs);
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
return false;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1108
1184
|
function truthyString(value) {
|
|
1109
1185
|
return /^(1|true|yes|on)$/iu.test(String(value || "").trim());
|
|
1110
1186
|
}
|
package/web/app.css
CHANGED
|
@@ -1541,6 +1541,90 @@ code {
|
|
|
1541
1541
|
color: rgba(205, 220, 231, 0.42);
|
|
1542
1542
|
}
|
|
1543
1543
|
|
|
1544
|
+
.reply-attachment-field {
|
|
1545
|
+
display: grid;
|
|
1546
|
+
gap: 0.55rem;
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
.reply-attachment-field__header {
|
|
1550
|
+
display: flex;
|
|
1551
|
+
align-items: center;
|
|
1552
|
+
justify-content: space-between;
|
|
1553
|
+
gap: 0.75rem;
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
.reply-attachment-picker {
|
|
1557
|
+
display: grid;
|
|
1558
|
+
gap: 0.18rem;
|
|
1559
|
+
padding: 0.88rem 0.95rem;
|
|
1560
|
+
border-radius: 18px;
|
|
1561
|
+
border: 1px dashed rgba(156, 181, 197, 0.24);
|
|
1562
|
+
background: rgba(255, 255, 255, 0.03);
|
|
1563
|
+
cursor: pointer;
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
.reply-attachment-picker__input {
|
|
1567
|
+
position: absolute;
|
|
1568
|
+
opacity: 0;
|
|
1569
|
+
pointer-events: none;
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
.reply-attachment-picker__label {
|
|
1573
|
+
font-size: 0.93rem;
|
|
1574
|
+
font-weight: 700;
|
|
1575
|
+
color: #eef8ff;
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
.reply-attachment-picker__hint {
|
|
1579
|
+
font-size: 0.82rem;
|
|
1580
|
+
color: var(--muted);
|
|
1581
|
+
line-height: 1.42;
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
.reply-image-preview {
|
|
1585
|
+
display: grid;
|
|
1586
|
+
grid-template-columns: 4.5rem minmax(0, 1fr);
|
|
1587
|
+
gap: 0.8rem;
|
|
1588
|
+
align-items: center;
|
|
1589
|
+
padding: 0.78rem 0.85rem;
|
|
1590
|
+
border-radius: 18px;
|
|
1591
|
+
border: 1px solid rgba(156, 181, 197, 0.12);
|
|
1592
|
+
background: rgba(255, 255, 255, 0.04);
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
.reply-image-preview__image {
|
|
1596
|
+
width: 4.5rem;
|
|
1597
|
+
height: 4.5rem;
|
|
1598
|
+
object-fit: cover;
|
|
1599
|
+
border-radius: 14px;
|
|
1600
|
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
1601
|
+
background: rgba(6, 10, 14, 0.92);
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
.reply-image-preview__copy {
|
|
1605
|
+
min-width: 0;
|
|
1606
|
+
display: grid;
|
|
1607
|
+
gap: 0.14rem;
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
.reply-image-preview__name,
|
|
1611
|
+
.reply-image-preview__meta {
|
|
1612
|
+
margin: 0;
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
.reply-image-preview__name {
|
|
1616
|
+
color: #eef8ff;
|
|
1617
|
+
font-size: 0.91rem;
|
|
1618
|
+
font-weight: 600;
|
|
1619
|
+
line-height: 1.35;
|
|
1620
|
+
overflow-wrap: anywhere;
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
.reply-image-preview__meta {
|
|
1624
|
+
color: var(--muted);
|
|
1625
|
+
font-size: 0.8rem;
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1544
1628
|
.reply-mode-switch {
|
|
1545
1629
|
display: grid;
|
|
1546
1630
|
grid-template-columns: auto auto minmax(0, 1fr);
|
package/web/app.js
CHANGED
|
@@ -87,7 +87,7 @@ async function boot() {
|
|
|
87
87
|
|
|
88
88
|
await refreshSession();
|
|
89
89
|
|
|
90
|
-
if (!state.session?.authenticated && initialPairToken) {
|
|
90
|
+
if (!state.session?.authenticated && initialPairToken && shouldAutoPairFromBootstrapToken()) {
|
|
91
91
|
try {
|
|
92
92
|
await pair({ token: initialPairToken });
|
|
93
93
|
} catch (error) {
|
|
@@ -480,6 +480,7 @@ function syncCompletedThreadFilter() {
|
|
|
480
480
|
}
|
|
481
481
|
|
|
482
482
|
function renderPair() {
|
|
483
|
+
const shouldInstallFromHomeScreen = Boolean(initialPairToken) && !shouldAutoPairFromBootstrapToken();
|
|
483
484
|
app.innerHTML = `
|
|
484
485
|
<main class="onboarding-shell">
|
|
485
486
|
<section class="onboarding-card">
|
|
@@ -488,6 +489,7 @@ function renderPair() {
|
|
|
488
489
|
<p class="hero-copy">${escapeHtml(L("pair.copy"))}</p>
|
|
489
490
|
${state.pairNotice ? `<p class="inline-alert inline-alert--success">${escapeHtml(state.pairNotice)}</p>` : ""}
|
|
490
491
|
${state.pairError ? `<p class="inline-alert inline-alert--danger">${escapeHtml(state.pairError)}</p>` : ""}
|
|
492
|
+
${shouldInstallFromHomeScreen ? `<p class="inline-alert inline-alert--warning">${escapeHtml(L("pair.installFromHomeScreen"))}</p>` : ""}
|
|
491
493
|
<form id="pair-form" class="pair-form">
|
|
492
494
|
<label class="field">
|
|
493
495
|
<span class="field-label">${escapeHtml(L("pair.codeLabel"))}</span>
|
|
@@ -555,6 +557,7 @@ function resetAuthenticatedState() {
|
|
|
555
557
|
state.detailLoadingItem = null;
|
|
556
558
|
state.detailOpen = false;
|
|
557
559
|
state.choiceLocalDrafts = {};
|
|
560
|
+
clearAllCompletionReplyDrafts();
|
|
558
561
|
state.completionReplyDrafts = {};
|
|
559
562
|
state.settingsSubpage = "";
|
|
560
563
|
state.settingsScrollState = null;
|
|
@@ -765,11 +768,70 @@ function normalizeReplyMode(value) {
|
|
|
765
768
|
return normalizeClientText(value).toLowerCase() === "plan" ? "plan" : "default";
|
|
766
769
|
}
|
|
767
770
|
|
|
771
|
+
const COMPLETION_REPLY_IMAGE_SUPPORT = false;
|
|
772
|
+
|
|
773
|
+
function normalizeCompletionReplyAttachment(value) {
|
|
774
|
+
if (!COMPLETION_REPLY_IMAGE_SUPPORT) {
|
|
775
|
+
return null;
|
|
776
|
+
}
|
|
777
|
+
if (!value || typeof value !== "object") {
|
|
778
|
+
return null;
|
|
779
|
+
}
|
|
780
|
+
const file = typeof File !== "undefined" && value.file instanceof File ? value.file : null;
|
|
781
|
+
const name = normalizeClientText(value.name || file?.name || "");
|
|
782
|
+
const type = normalizeClientText(value.type || file?.type || "");
|
|
783
|
+
const size = Number(value.size ?? file?.size) || 0;
|
|
784
|
+
const previewUrl = normalizeClientText(value.previewUrl || "");
|
|
785
|
+
if (!file || !name || !type.startsWith("image/") || size <= 0) {
|
|
786
|
+
return null;
|
|
787
|
+
}
|
|
788
|
+
return {
|
|
789
|
+
file,
|
|
790
|
+
name,
|
|
791
|
+
type,
|
|
792
|
+
size,
|
|
793
|
+
previewUrl,
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
function createCompletionReplyAttachment(file) {
|
|
798
|
+
if (!COMPLETION_REPLY_IMAGE_SUPPORT) {
|
|
799
|
+
return null;
|
|
800
|
+
}
|
|
801
|
+
if (!(typeof File !== "undefined" && file instanceof File)) {
|
|
802
|
+
return null;
|
|
803
|
+
}
|
|
804
|
+
if (!normalizeClientText(file.type).startsWith("image/")) {
|
|
805
|
+
return null;
|
|
806
|
+
}
|
|
807
|
+
return normalizeCompletionReplyAttachment({
|
|
808
|
+
file,
|
|
809
|
+
name: file.name,
|
|
810
|
+
type: file.type,
|
|
811
|
+
size: file.size,
|
|
812
|
+
previewUrl: typeof URL !== "undefined" && typeof URL.createObjectURL === "function"
|
|
813
|
+
? URL.createObjectURL(file)
|
|
814
|
+
: "",
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function releaseCompletionReplyAttachment(attachment) {
|
|
819
|
+
if (!attachment?.previewUrl || typeof URL === "undefined" || typeof URL.revokeObjectURL !== "function") {
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
try {
|
|
823
|
+
URL.revokeObjectURL(attachment.previewUrl);
|
|
824
|
+
} catch {
|
|
825
|
+
// Ignore best-effort object URL cleanup errors.
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
768
829
|
function getCompletionReplyDraft(token) {
|
|
769
830
|
if (!token) {
|
|
770
831
|
return {
|
|
771
832
|
text: "",
|
|
772
833
|
sentText: "",
|
|
834
|
+
attachment: null,
|
|
773
835
|
mode: "default",
|
|
774
836
|
notice: "",
|
|
775
837
|
error: "",
|
|
@@ -784,6 +846,7 @@ function getCompletionReplyDraft(token) {
|
|
|
784
846
|
return {
|
|
785
847
|
text: String(draft.text ?? ""),
|
|
786
848
|
sentText: normalizeClientText(draft.sentText ?? ""),
|
|
849
|
+
attachment: normalizeCompletionReplyAttachment(draft.attachment),
|
|
787
850
|
mode: normalizeReplyMode(draft.mode),
|
|
788
851
|
notice: normalizeClientText(draft.notice),
|
|
789
852
|
error: normalizeClientText(draft.error),
|
|
@@ -815,13 +878,22 @@ function setCompletionReplyDraft(token, partialDraft) {
|
|
|
815
878
|
if (!token) {
|
|
816
879
|
return;
|
|
817
880
|
}
|
|
881
|
+
const previousStoredDraft = state.completionReplyDrafts?.[token] || {};
|
|
882
|
+
const previousAttachment = normalizeCompletionReplyAttachment(previousStoredDraft.attachment);
|
|
818
883
|
const nextDraft = {
|
|
819
884
|
...getCompletionReplyDraft(token),
|
|
820
885
|
...(partialDraft || {}),
|
|
821
886
|
};
|
|
887
|
+
const nextAttachment = Object.prototype.hasOwnProperty.call(partialDraft || {}, "attachment")
|
|
888
|
+
? normalizeCompletionReplyAttachment(partialDraft?.attachment)
|
|
889
|
+
: normalizeCompletionReplyAttachment(nextDraft.attachment);
|
|
890
|
+
if (previousAttachment?.previewUrl && previousAttachment.previewUrl !== nextAttachment?.previewUrl) {
|
|
891
|
+
releaseCompletionReplyAttachment(previousAttachment);
|
|
892
|
+
}
|
|
822
893
|
state.completionReplyDrafts[token] = {
|
|
823
894
|
text: String(nextDraft.text ?? ""),
|
|
824
895
|
sentText: normalizeClientText(nextDraft.sentText ?? ""),
|
|
896
|
+
attachment: nextAttachment,
|
|
825
897
|
mode: normalizeReplyMode(nextDraft.mode),
|
|
826
898
|
notice: normalizeClientText(nextDraft.notice),
|
|
827
899
|
error: normalizeClientText(nextDraft.error),
|
|
@@ -836,9 +908,16 @@ function clearCompletionReplyDraft(token) {
|
|
|
836
908
|
if (!token || !state.completionReplyDrafts?.[token]) {
|
|
837
909
|
return;
|
|
838
910
|
}
|
|
911
|
+
releaseCompletionReplyAttachment(state.completionReplyDrafts[token]?.attachment);
|
|
839
912
|
delete state.completionReplyDrafts[token];
|
|
840
913
|
}
|
|
841
914
|
|
|
915
|
+
function clearAllCompletionReplyDrafts() {
|
|
916
|
+
for (const token of Object.keys(state.completionReplyDrafts || {})) {
|
|
917
|
+
clearCompletionReplyDraft(token);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
842
921
|
function syncCompletionReplyComposerLiveState(replyForm, draft) {
|
|
843
922
|
if (!replyForm) {
|
|
844
923
|
return;
|
|
@@ -2210,6 +2289,8 @@ function renderCompletionReplyComposer(detail, options = {}) {
|
|
|
2210
2289
|
const warningTimestamp = draft.warning?.createdAtMs ? formatTimelineTimestamp(draft.warning.createdAtMs) : "";
|
|
2211
2290
|
const showCollapsedState =
|
|
2212
2291
|
draft.collapsedAfterSend && Boolean(draft.notice) && !draft.error && !draft.warning && !draft.sending;
|
|
2292
|
+
const attachmentName = draft.attachment?.name ? escapeHtml(draft.attachment.name) : "";
|
|
2293
|
+
const attachmentPreviewUrl = draft.attachment?.previewUrl ? escapeHtml(draft.attachment.previewUrl) : "";
|
|
2213
2294
|
|
|
2214
2295
|
return `
|
|
2215
2296
|
<section class="detail-card detail-card--reply ${options.mobile ? "detail-card--mobile" : ""}">
|
|
@@ -2275,6 +2356,55 @@ function renderCompletionReplyComposer(detail, options = {}) {
|
|
|
2275
2356
|
data-reply-token="${escapeHtml(detail.token)}"
|
|
2276
2357
|
>${escapeHtml(draft.text)}</textarea>
|
|
2277
2358
|
</label>
|
|
2359
|
+
${
|
|
2360
|
+
detail.reply?.supportsImages
|
|
2361
|
+
? `
|
|
2362
|
+
<div class="reply-attachment-field">
|
|
2363
|
+
<div class="reply-attachment-field__header">
|
|
2364
|
+
<span class="field-label">${escapeHtml(L("reply.imageLabel"))}</span>
|
|
2365
|
+
${
|
|
2366
|
+
draft.attachment
|
|
2367
|
+
? `
|
|
2368
|
+
<button
|
|
2369
|
+
class="secondary secondary--compact"
|
|
2370
|
+
type="button"
|
|
2371
|
+
data-reply-image-remove
|
|
2372
|
+
data-reply-token="${escapeHtml(detail.token)}"
|
|
2373
|
+
>
|
|
2374
|
+
${escapeHtml(L("reply.imageRemove"))}
|
|
2375
|
+
</button>
|
|
2376
|
+
`
|
|
2377
|
+
: ""
|
|
2378
|
+
}
|
|
2379
|
+
</div>
|
|
2380
|
+
<label class="reply-attachment-picker">
|
|
2381
|
+
<input
|
|
2382
|
+
class="reply-attachment-picker__input"
|
|
2383
|
+
type="file"
|
|
2384
|
+
accept="image/*"
|
|
2385
|
+
data-reply-image-input
|
|
2386
|
+
data-reply-token="${escapeHtml(detail.token)}"
|
|
2387
|
+
>
|
|
2388
|
+
<span class="reply-attachment-picker__label">${escapeHtml(L(draft.attachment ? "reply.imageReplace" : "reply.imageAdd"))}</span>
|
|
2389
|
+
<span class="reply-attachment-picker__hint">${escapeHtml(L("reply.imageHint"))}</span>
|
|
2390
|
+
</label>
|
|
2391
|
+
${
|
|
2392
|
+
draft.attachment
|
|
2393
|
+
? `
|
|
2394
|
+
<div class="reply-image-preview">
|
|
2395
|
+
<img class="reply-image-preview__image" src="${attachmentPreviewUrl}" alt="${attachmentName}">
|
|
2396
|
+
<div class="reply-image-preview__copy">
|
|
2397
|
+
<p class="reply-image-preview__name">${attachmentName}</p>
|
|
2398
|
+
<p class="reply-image-preview__meta">${escapeHtml(L("reply.imageAttached"))}</p>
|
|
2399
|
+
</div>
|
|
2400
|
+
</div>
|
|
2401
|
+
`
|
|
2402
|
+
: ""
|
|
2403
|
+
}
|
|
2404
|
+
</div>
|
|
2405
|
+
`
|
|
2406
|
+
: ""
|
|
2407
|
+
}
|
|
2278
2408
|
${
|
|
2279
2409
|
detail.reply?.supportsPlanMode
|
|
2280
2410
|
? `
|
|
@@ -2841,6 +2971,46 @@ function bindShellInteractions() {
|
|
|
2841
2971
|
});
|
|
2842
2972
|
}
|
|
2843
2973
|
|
|
2974
|
+
for (const input of document.querySelectorAll("[data-reply-image-input][data-reply-token]")) {
|
|
2975
|
+
input.addEventListener("change", async () => {
|
|
2976
|
+
const token = input.dataset.replyToken || "";
|
|
2977
|
+
const [file] = Array.from(input.files || []);
|
|
2978
|
+
const nextAttachment = createCompletionReplyAttachment(file);
|
|
2979
|
+
if (!nextAttachment && file) {
|
|
2980
|
+
setCompletionReplyDraft(token, {
|
|
2981
|
+
error: L("error.completionReplyImageInvalidType"),
|
|
2982
|
+
notice: "",
|
|
2983
|
+
warning: null,
|
|
2984
|
+
confirmOverride: false,
|
|
2985
|
+
});
|
|
2986
|
+
await renderShell();
|
|
2987
|
+
return;
|
|
2988
|
+
}
|
|
2989
|
+
setCompletionReplyDraft(token, {
|
|
2990
|
+
attachment: nextAttachment,
|
|
2991
|
+
notice: "",
|
|
2992
|
+
error: "",
|
|
2993
|
+
warning: null,
|
|
2994
|
+
confirmOverride: false,
|
|
2995
|
+
});
|
|
2996
|
+
await renderShell();
|
|
2997
|
+
});
|
|
2998
|
+
}
|
|
2999
|
+
|
|
3000
|
+
for (const button of document.querySelectorAll("[data-reply-image-remove][data-reply-token]")) {
|
|
3001
|
+
button.addEventListener("click", async () => {
|
|
3002
|
+
const token = button.dataset.replyToken || "";
|
|
3003
|
+
setCompletionReplyDraft(token, {
|
|
3004
|
+
attachment: null,
|
|
3005
|
+
notice: "",
|
|
3006
|
+
error: "",
|
|
3007
|
+
warning: null,
|
|
3008
|
+
confirmOverride: false,
|
|
3009
|
+
});
|
|
3010
|
+
await renderShell();
|
|
3011
|
+
});
|
|
3012
|
+
}
|
|
3013
|
+
|
|
2844
3014
|
for (const button of document.querySelectorAll("[data-open-logout-confirm]")) {
|
|
2845
3015
|
button.addEventListener("click", async () => {
|
|
2846
3016
|
state.logoutConfirmOpen = true;
|
|
@@ -3010,14 +3180,18 @@ function bindShellInteractions() {
|
|
|
3010
3180
|
await renderShell();
|
|
3011
3181
|
|
|
3012
3182
|
try {
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3183
|
+
const requestBody = new FormData();
|
|
3184
|
+
requestBody.set("text", text);
|
|
3185
|
+
requestBody.set("planMode", draft.mode === "plan" ? "true" : "false");
|
|
3186
|
+
requestBody.set("force", draft.confirmOverride === true ? "true" : "false");
|
|
3187
|
+
if (COMPLETION_REPLY_IMAGE_SUPPORT && draft.attachment?.file) {
|
|
3188
|
+
requestBody.append("image", draft.attachment.file, draft.attachment.name || draft.attachment.file.name);
|
|
3189
|
+
}
|
|
3190
|
+
await apiPost(`/api/items/completion/${encodeURIComponent(token)}/reply`, requestBody);
|
|
3018
3191
|
setCompletionReplyDraft(token, {
|
|
3019
3192
|
text: "",
|
|
3020
3193
|
sentText: text,
|
|
3194
|
+
attachment: null,
|
|
3021
3195
|
mode: draft.mode,
|
|
3022
3196
|
sending: false,
|
|
3023
3197
|
error: "",
|
|
@@ -3032,6 +3206,7 @@ function bindShellInteractions() {
|
|
|
3032
3206
|
setCompletionReplyDraft(token, {
|
|
3033
3207
|
text,
|
|
3034
3208
|
sentText: "",
|
|
3209
|
+
attachment: draft.attachment,
|
|
3035
3210
|
mode: draft.mode,
|
|
3036
3211
|
sending: false,
|
|
3037
3212
|
notice: "",
|
|
@@ -3046,6 +3221,7 @@ function bindShellInteractions() {
|
|
|
3046
3221
|
setCompletionReplyDraft(token, {
|
|
3047
3222
|
text,
|
|
3048
3223
|
sentText: "",
|
|
3224
|
+
attachment: draft.attachment,
|
|
3049
3225
|
mode: draft.mode,
|
|
3050
3226
|
sending: false,
|
|
3051
3227
|
notice: "",
|
|
@@ -3759,14 +3935,19 @@ async function apiGet(url) {
|
|
|
3759
3935
|
}
|
|
3760
3936
|
|
|
3761
3937
|
async function apiPost(url, body) {
|
|
3938
|
+
const isFormDataBody = typeof FormData !== "undefined" && body instanceof FormData;
|
|
3762
3939
|
const response = await fetch(url, {
|
|
3763
3940
|
method: "POST",
|
|
3764
3941
|
credentials: "same-origin",
|
|
3765
|
-
headers:
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3942
|
+
headers: isFormDataBody
|
|
3943
|
+
? {
|
|
3944
|
+
Accept: "application/json",
|
|
3945
|
+
}
|
|
3946
|
+
: {
|
|
3947
|
+
"Content-Type": "application/json",
|
|
3948
|
+
Accept: "application/json",
|
|
3949
|
+
},
|
|
3950
|
+
body: isFormDataBody ? body : JSON.stringify(body || {}),
|
|
3770
3951
|
});
|
|
3771
3952
|
if (!response.ok) {
|
|
3772
3953
|
const errorInfo = await readError(response);
|
|
@@ -3809,6 +3990,11 @@ function localizeApiError(value) {
|
|
|
3809
3990
|
"completion-reply-unavailable": "error.completionReplyUnavailable",
|
|
3810
3991
|
"completion-reply-thread-advanced": "error.completionReplyThreadAdvanced",
|
|
3811
3992
|
"completion-reply-empty": "error.completionReplyEmpty",
|
|
3993
|
+
"completion-reply-image-invalid-type": "error.completionReplyImageInvalidType",
|
|
3994
|
+
"completion-reply-image-too-large": "error.completionReplyImageTooLarge",
|
|
3995
|
+
"completion-reply-image-limit": "error.completionReplyImageLimit",
|
|
3996
|
+
"completion-reply-image-invalid-upload": "error.completionReplyImageInvalidUpload",
|
|
3997
|
+
"completion-reply-image-disabled": "error.completionReplyImageDisabled",
|
|
3812
3998
|
"codex-ipc-not-connected": "error.codexIpcNotConnected",
|
|
3813
3999
|
"approval-not-found": "error.approvalNotFound",
|
|
3814
4000
|
"approval-already-handled": "error.approvalAlreadyHandled",
|
|
@@ -3889,6 +4075,19 @@ function desiredBootstrapPairingToken() {
|
|
|
3889
4075
|
return initialPairToken;
|
|
3890
4076
|
}
|
|
3891
4077
|
|
|
4078
|
+
function shouldAutoPairFromBootstrapToken() {
|
|
4079
|
+
if (!initialPairToken) {
|
|
4080
|
+
return false;
|
|
4081
|
+
}
|
|
4082
|
+
if (isStandaloneMode()) {
|
|
4083
|
+
return true;
|
|
4084
|
+
}
|
|
4085
|
+
if (isProbablySafari()) {
|
|
4086
|
+
return false;
|
|
4087
|
+
}
|
|
4088
|
+
return true;
|
|
4089
|
+
}
|
|
4090
|
+
|
|
3892
4091
|
function urlBase64ToUint8Array(base64String) {
|
|
3893
4092
|
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
|
|
3894
4093
|
const normalized = `${base64String}${padding}`.replace(/-/gu, "+").replace(/_/gu, "/");
|
package/web/i18n.js
CHANGED
|
@@ -70,6 +70,8 @@ const translations = {
|
|
|
70
70
|
"pair.codeLabel": "Pairing code",
|
|
71
71
|
"pair.codePlaceholder": "Enter the pairing code",
|
|
72
72
|
"pair.connect": "Connect this iPhone",
|
|
73
|
+
"pair.installFromHomeScreen":
|
|
74
|
+
"This pairing link is being kept for the Home Screen app. Add viveworker to your Home Screen from Safari, then open the new icon to finish pairing there.",
|
|
73
75
|
"pair.helperTitle": "Add to Home Screen",
|
|
74
76
|
"pair.helperCopy": "Install viveworker for the best mobile layout and Web Push support.",
|
|
75
77
|
"banner.install.title": "Add viveworker to your Home Screen",
|
|
@@ -112,6 +114,12 @@ const translations = {
|
|
|
112
114
|
"reply.copy": "Send a new message back into this Codex thread from the latest completed result.",
|
|
113
115
|
"reply.fieldLabel": "Message",
|
|
114
116
|
"reply.placeholder": "Ask Codex to continue, refine the result, or try a different approach.",
|
|
117
|
+
"reply.imageLabel": "Image",
|
|
118
|
+
"reply.imageAdd": "Add image",
|
|
119
|
+
"reply.imageReplace": "Replace image",
|
|
120
|
+
"reply.imageRemove": "Remove",
|
|
121
|
+
"reply.imageHint": "Attach one image from your iPhone and send it together with your message.",
|
|
122
|
+
"reply.imageAttached": "Attached image",
|
|
115
123
|
"reply.send": "Send to Codex",
|
|
116
124
|
"reply.sendConfirm": "Send anyway",
|
|
117
125
|
"reply.sendAnother": "Send another message",
|
|
@@ -283,6 +291,11 @@ const translations = {
|
|
|
283
291
|
"error.completionReplyUnavailable": "This completed item can no longer send a follow-up.",
|
|
284
292
|
"error.completionReplyThreadAdvanced": "This thread already has newer messages.",
|
|
285
293
|
"error.completionReplyEmpty": "Enter a message before sending it.",
|
|
294
|
+
"error.completionReplyImageInvalidType": "Choose an image file to attach.",
|
|
295
|
+
"error.completionReplyImageTooLarge": "That image is too large to send from this reply screen.",
|
|
296
|
+
"error.completionReplyImageLimit": "Attach one image at a time for now.",
|
|
297
|
+
"error.completionReplyImageInvalidUpload": "That image could not be uploaded.",
|
|
298
|
+
"error.completionReplyImageDisabled": "Image attachments are not available in this release.",
|
|
286
299
|
"error.codexIpcNotConnected": "Codex desktop is not connected right now.",
|
|
287
300
|
"error.approvalNotFound": "This approval is no longer available.",
|
|
288
301
|
"error.approvalAlreadyHandled": "This approval was already handled.",
|
|
@@ -394,6 +407,8 @@ const translations = {
|
|
|
394
407
|
"logout.action.removeDevice": "Log out and remove this device",
|
|
395
408
|
"cli.setup.warning.insecureHttpLan":
|
|
396
409
|
"Warning: insecure LAN HTTP is enabled. Session and device cookies can be exposed on the local network.",
|
|
410
|
+
"cli.setup.warning.stalePairingServer":
|
|
411
|
+
"Warning: viveworker is responding on port {port}, but it is still serving older pairing credentials. Another bridge process may still own this port. Stop the older instance, then rerun setup or start.",
|
|
397
412
|
"cli.setup.qrPairing": "Pairing QR:",
|
|
398
413
|
"cli.setup.qrCaDownload": "rootCA.pem download QR (IP):",
|
|
399
414
|
"cli.setup.prompt.continueToApp": "Press Enter to continue to the viveworker app URL.",
|
|
@@ -509,6 +524,8 @@ const translations = {
|
|
|
509
524
|
"pair.codeLabel": "ペアリングコード",
|
|
510
525
|
"pair.codePlaceholder": "ペアリングコードを入力",
|
|
511
526
|
"pair.connect": "この iPhone を接続",
|
|
527
|
+
"pair.installFromHomeScreen":
|
|
528
|
+
"この pairing link はホーム画面アプリ用に温存されています。Safari から viveworker をホーム画面に追加し、新しいアイコンを開いてそこで pairing を完了してください。",
|
|
512
529
|
"pair.helperTitle": "ホーム画面に追加",
|
|
513
530
|
"pair.helperCopy": "モバイル表示と Web Push を最適に使うには viveworker をインストールしてください。",
|
|
514
531
|
"banner.install.title": "viveworker をホーム画面に追加",
|
|
@@ -550,6 +567,12 @@ const translations = {
|
|
|
550
567
|
"reply.copy": "この完了結果を見ながら、同じ Codex スレッドに新しいメッセージを送れます。",
|
|
551
568
|
"reply.fieldLabel": "メッセージ",
|
|
552
569
|
"reply.placeholder": "続きを依頼したり、結果の修正や別案を Codex に伝えます。",
|
|
570
|
+
"reply.imageLabel": "画像",
|
|
571
|
+
"reply.imageAdd": "画像を追加",
|
|
572
|
+
"reply.imageReplace": "画像を差し替え",
|
|
573
|
+
"reply.imageRemove": "削除",
|
|
574
|
+
"reply.imageHint": "iPhone の画像を 1 枚だけ添付して、メッセージと一緒に送れます。",
|
|
575
|
+
"reply.imageAttached": "添付した画像",
|
|
553
576
|
"reply.send": "Codex に送信",
|
|
554
577
|
"reply.sendConfirm": "それでも送信",
|
|
555
578
|
"reply.sendAnother": "もう一度送る",
|
|
@@ -721,6 +744,11 @@ const translations = {
|
|
|
721
744
|
"error.completionReplyUnavailable": "この完了項目には、もう返信できません。",
|
|
722
745
|
"error.completionReplyThreadAdvanced": "このスレッドには、すでに新しいメッセージがあります。",
|
|
723
746
|
"error.completionReplyEmpty": "送信前にメッセージを入力してください。",
|
|
747
|
+
"error.completionReplyImageInvalidType": "添付できるのは画像ファイルのみです。",
|
|
748
|
+
"error.completionReplyImageTooLarge": "この画面から送るには画像が大きすぎます。",
|
|
749
|
+
"error.completionReplyImageLimit": "いまは画像を 1 枚ずつ添付してください。",
|
|
750
|
+
"error.completionReplyImageInvalidUpload": "この画像はアップロードできませんでした。",
|
|
751
|
+
"error.completionReplyImageDisabled": "このリリースでは画像添付はまだ利用できません。",
|
|
724
752
|
"error.codexIpcNotConnected": "いまは Codex desktop に接続できていません。",
|
|
725
753
|
"error.approvalNotFound": "この承認はもう利用できません。",
|
|
726
754
|
"error.approvalAlreadyHandled": "この承認はすでに処理済みです。",
|
|
@@ -832,6 +860,8 @@ const translations = {
|
|
|
832
860
|
"logout.action.removeDevice": "ログアウトしてこの端末を削除",
|
|
833
861
|
"cli.setup.warning.insecureHttpLan":
|
|
834
862
|
"警告: insecure LAN HTTP が有効です。session / device cookie がローカルネットワーク上で露出する可能性があります。",
|
|
863
|
+
"cli.setup.warning.stalePairingServer":
|
|
864
|
+
"警告: port {port} の viveworker は応答していますが、古い pairing 情報を配信したままです。別の bridge process がこの port を使っている可能性があります。古い instance を停止してから setup または start をやり直してください。",
|
|
835
865
|
"cli.setup.qrPairing": "Pairing QR:",
|
|
836
866
|
"cli.setup.qrCaDownload": "rootCA.pem download QR (IP):",
|
|
837
867
|
"cli.setup.prompt.continueToApp": "viveworker の接続先 URL を表示するには Enter を押してください。",
|
package/web/sw.js
CHANGED