@cjhyy/code-shell-web 0.9.26 → 0.9.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/lib/outputJournalRecovery.d.ts +28 -0
- package/dist/lib/outputJournalRecovery.js +148 -0
- package/dist/lib/streamReducer.d.ts +1 -0
- package/dist/lib/streamReducer.js +12 -1
- package/dist/lib/transcriptReplay.d.ts +2 -7
- package/dist/lib/transcriptReplay.js +3 -85
- package/dist/lib/userMessageDisplay.d.ts +17 -0
- package/dist/lib/userMessageDisplay.js +130 -0
- package/dist/link-authorization-view.d.ts +48 -0
- package/dist/link-authorization-view.js +178 -0
- package/dist/link-authorization.d.ts +51 -0
- package/dist/link-authorization.js +273 -0
- package/dist-app/assets/index-Do7zJ7tU.js +57 -0
- package/dist-app/assets/index-EIfqZrcz.css +1 -0
- package/dist-app/index.html +2 -2
- package/package.json +6 -4
- package/dist-app/assets/index-C9YhaFPF.js +0 -50
- package/dist-app/assets/index-Dtx33DyF.css +0 -1
package/dist/index.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export { useRemoteSocket, type ConnStatus, type ResyncReason, type RemoteSocket,
|
|
|
13
13
|
export * from "./hooks/remoteAppSync.js";
|
|
14
14
|
export * from "./lib/streamReducer.js";
|
|
15
15
|
export * from "./lib/transcriptReplay.js";
|
|
16
|
+
export * from "./lib/outputJournalRecovery.js";
|
|
16
17
|
export * from "./lib/messageMappers.js";
|
|
17
18
|
export * from "./lib/riskClassify.js";
|
|
18
19
|
export * from "./lib/format.js";
|
|
@@ -21,6 +22,8 @@ export * from "./lib/pairing.js";
|
|
|
21
22
|
export * from "./lib/deviceCredential.js";
|
|
22
23
|
export * from "./lib/mobileAttachments.js";
|
|
23
24
|
export * from "./lib/uiLanguage.js";
|
|
25
|
+
export * from "./link-authorization.js";
|
|
26
|
+
export * from "./link-authorization-view.js";
|
|
24
27
|
export { mobile } from "./i18n/mobile.js";
|
|
25
28
|
export { translate, t, webMessages, type WebTranslationKey, type WebTranslateParams, } from "./i18n/translate.js";
|
|
26
29
|
export { isEnvironmentDescriptor, projectReferenceKey } from "./lib/environment.js";
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,7 @@ export * from "./hooks/remoteAppSync.js";
|
|
|
15
15
|
// ── Stream folding (shared with the desktop renderer's CC room view) ────────
|
|
16
16
|
export * from "./lib/streamReducer.js";
|
|
17
17
|
export * from "./lib/transcriptReplay.js";
|
|
18
|
+
export * from "./lib/outputJournalRecovery.js";
|
|
18
19
|
export * from "./lib/messageMappers.js";
|
|
19
20
|
// ── Pure helpers ─────────────────────────────────────────────────────────────
|
|
20
21
|
export * from "./lib/riskClassify.js";
|
|
@@ -24,6 +25,8 @@ export * from "./lib/pairing.js";
|
|
|
24
25
|
export * from "./lib/deviceCredential.js";
|
|
25
26
|
export * from "./lib/mobileAttachments.js";
|
|
26
27
|
export * from "./lib/uiLanguage.js";
|
|
28
|
+
export * from "./link-authorization.js";
|
|
29
|
+
export * from "./link-authorization-view.js";
|
|
27
30
|
// ── i18n: mobile namespace dict + zh-fallback translate ─────────────────────
|
|
28
31
|
export { mobile } from "./i18n/mobile.js";
|
|
29
32
|
export { translate, t, webMessages, } from "./i18n/translate.js";
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { StreamEvent } from "@cjhyy/code-shell-core";
|
|
2
|
+
import type { OutputJournalPage } from "@cjhyy/code-shell-core/internal";
|
|
3
|
+
/** Per-recovery state; keep it separate from Main/Hub transport sequence state. */
|
|
4
|
+
export interface OutputJournalRecovery {
|
|
5
|
+
cursor?: string;
|
|
6
|
+
/** Safe to persist across browser restarts; never points inside a fragmented event. */
|
|
7
|
+
appliedCursor?: string;
|
|
8
|
+
through?: string;
|
|
9
|
+
coverageStart?: string;
|
|
10
|
+
legacyBaseThroughEventId?: string;
|
|
11
|
+
incomplete: boolean;
|
|
12
|
+
pending?: {
|
|
13
|
+
id: string;
|
|
14
|
+
total: number;
|
|
15
|
+
next: number;
|
|
16
|
+
bytes: number;
|
|
17
|
+
chunks: Uint8Array[];
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/** Only compare positions in the same Session journal, never Host counters. */
|
|
21
|
+
export declare function compareOutputCursors(left: string, right: string): number | undefined;
|
|
22
|
+
/**
|
|
23
|
+
* Fold a frozen, contiguous page exactly once. Callers first load legacy history
|
|
24
|
+
* through legacyBaseThroughEventId, then apply returned StreamEvents to their
|
|
25
|
+
* existing reducer. An unpaired legacy transcript remains behind its barrier.
|
|
26
|
+
* This function neither treats EOF as completion nor changes transport epochs.
|
|
27
|
+
*/
|
|
28
|
+
export declare function applyOutputJournalPage(state: OutputJournalRecovery, page: OutputJournalPage, emit: (event: StreamEvent) => void): boolean;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { projectOutputUserEvent } from "./userMessageDisplay.js";
|
|
2
|
+
const MAX_EVENT_BYTES = 16 * 1024 * 1024;
|
|
3
|
+
function position(token) {
|
|
4
|
+
if (typeof token !== "string" || token.length > 2048 || !/^[A-Za-z0-9_-]+$/.test(token))
|
|
5
|
+
throw new Error("Invalid output cursor");
|
|
6
|
+
const parsed = JSON.parse(atob(token.replace(/-/g, "+").replace(/_/g, "/")));
|
|
7
|
+
if (typeof parsed.identity !== "string" ||
|
|
8
|
+
!Number.isSafeInteger(parsed.sequence) ||
|
|
9
|
+
parsed.sequence < 0 ||
|
|
10
|
+
!Number.isSafeInteger(parsed.offset) ||
|
|
11
|
+
parsed.offset < 0)
|
|
12
|
+
throw new Error("Invalid output cursor");
|
|
13
|
+
return parsed;
|
|
14
|
+
}
|
|
15
|
+
function decodeFragment(value) {
|
|
16
|
+
if (value.length > Math.ceil((64 * 1024) / 3) * 4 || !/^[A-Za-z0-9+/]*={0,2}$/.test(value))
|
|
17
|
+
throw new Error("Invalid output fragment");
|
|
18
|
+
const binary = atob(value);
|
|
19
|
+
return Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
|
20
|
+
}
|
|
21
|
+
/** Only compare positions in the same Session journal, never Host counters. */
|
|
22
|
+
export function compareOutputCursors(left, right) {
|
|
23
|
+
try {
|
|
24
|
+
const a = position(left), b = position(right);
|
|
25
|
+
return a.identity === b.identity ? Math.sign(a.sequence - b.sequence) : undefined;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Fold a frozen, contiguous page exactly once. Callers first load legacy history
|
|
33
|
+
* through legacyBaseThroughEventId, then apply returned StreamEvents to their
|
|
34
|
+
* existing reducer. An unpaired legacy transcript remains behind its barrier.
|
|
35
|
+
* This function neither treats EOF as completion nor changes transport epochs.
|
|
36
|
+
*/
|
|
37
|
+
export function applyOutputJournalPage(state, page, emit) {
|
|
38
|
+
if (state.incomplete)
|
|
39
|
+
return false;
|
|
40
|
+
try {
|
|
41
|
+
if (page.version !== 1 ||
|
|
42
|
+
page.status !== "ok" ||
|
|
43
|
+
!page.from ||
|
|
44
|
+
!page.through ||
|
|
45
|
+
!page.next ||
|
|
46
|
+
!page.coverageStart ||
|
|
47
|
+
!Array.isArray(page.frames) ||
|
|
48
|
+
page.frames.length > 512 ||
|
|
49
|
+
new TextEncoder().encode(JSON.stringify(page)).length > 1024 * 1024 + 16 * 1024)
|
|
50
|
+
throw new Error("Output recovery is incomplete");
|
|
51
|
+
if ((state.cursor ?? page.coverageStart) !== page.from ||
|
|
52
|
+
(state.through && state.through !== page.through) ||
|
|
53
|
+
(state.coverageStart && state.coverageStart !== page.coverageStart) ||
|
|
54
|
+
(state.legacyBaseThroughEventId !== undefined &&
|
|
55
|
+
state.legacyBaseThroughEventId !== page.legacyBaseThroughEventId))
|
|
56
|
+
throw new Error("Output recovery page changed its snapshot");
|
|
57
|
+
let previous = position(page.from);
|
|
58
|
+
const through = position(page.through);
|
|
59
|
+
const start = position(page.coverageStart);
|
|
60
|
+
if (start.sequence !== 0 ||
|
|
61
|
+
start.identity !== previous.identity ||
|
|
62
|
+
through.identity !== previous.identity ||
|
|
63
|
+
previous.sequence > through.sequence)
|
|
64
|
+
throw new Error("Output recovery cursor domain changed");
|
|
65
|
+
let pending = state.pending
|
|
66
|
+
? { ...state.pending, chunks: state.pending.chunks.slice() }
|
|
67
|
+
: undefined;
|
|
68
|
+
let appliedCursor = state.appliedCursor ?? page.from;
|
|
69
|
+
const events = [];
|
|
70
|
+
for (const frame of page.frames) {
|
|
71
|
+
const next = position(frame.cursor);
|
|
72
|
+
if (next.identity !== previous.identity ||
|
|
73
|
+
next.sequence !== previous.sequence + 1 ||
|
|
74
|
+
frame.sequence !== next.sequence ||
|
|
75
|
+
next.offset <= previous.offset ||
|
|
76
|
+
next.sequence > through.sequence ||
|
|
77
|
+
!!frame.event === !!frame.fragment)
|
|
78
|
+
throw new Error("Output recovery has a gap");
|
|
79
|
+
let event = frame.event;
|
|
80
|
+
if (frame.fragment) {
|
|
81
|
+
const part = frame.fragment;
|
|
82
|
+
if (!pending) {
|
|
83
|
+
if (part.index !== 0 ||
|
|
84
|
+
!Number.isSafeInteger(part.total) ||
|
|
85
|
+
part.total < 2 ||
|
|
86
|
+
part.total > 256)
|
|
87
|
+
throw new Error("Output fragment has no beginning");
|
|
88
|
+
pending = { id: part.id, total: part.total, next: 0, bytes: 0, chunks: [] };
|
|
89
|
+
}
|
|
90
|
+
if (part.id !== pending.id || part.total !== pending.total || part.index !== pending.next)
|
|
91
|
+
throw new Error("Output fragment has a gap");
|
|
92
|
+
const bytes = decodeFragment(part.data);
|
|
93
|
+
pending.bytes += bytes.length;
|
|
94
|
+
if (pending.bytes > MAX_EVENT_BYTES)
|
|
95
|
+
throw new Error("Output fragment exceeds its budget");
|
|
96
|
+
pending.chunks.push(bytes);
|
|
97
|
+
pending.next++;
|
|
98
|
+
if (pending.next === pending.total) {
|
|
99
|
+
const data = new Uint8Array(pending.bytes);
|
|
100
|
+
let offset = 0;
|
|
101
|
+
for (const chunk of pending.chunks) {
|
|
102
|
+
data.set(chunk, offset);
|
|
103
|
+
offset += chunk.length;
|
|
104
|
+
}
|
|
105
|
+
event = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(data));
|
|
106
|
+
pending = undefined;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
else if (pending)
|
|
110
|
+
throw new Error("Output fragment was interrupted");
|
|
111
|
+
if (event) {
|
|
112
|
+
if (typeof event !== "object" ||
|
|
113
|
+
typeof event.type !== "string" ||
|
|
114
|
+
event.outputRecovery === "incomplete")
|
|
115
|
+
throw new Error("Invalid output event");
|
|
116
|
+
events.push({
|
|
117
|
+
...projectOutputUserEvent(event),
|
|
118
|
+
outputCursor: frame.cursor,
|
|
119
|
+
});
|
|
120
|
+
appliedCursor = frame.cursor;
|
|
121
|
+
}
|
|
122
|
+
previous = next;
|
|
123
|
+
}
|
|
124
|
+
const next = position(page.next);
|
|
125
|
+
if (next.identity !== previous.identity ||
|
|
126
|
+
next.sequence !== previous.sequence ||
|
|
127
|
+
next.offset !== previous.offset ||
|
|
128
|
+
(page.frames.at(-1)?.cursor ?? page.from) !== page.next ||
|
|
129
|
+
page.complete !== (page.next === page.through) ||
|
|
130
|
+
(page.complete && pending) ||
|
|
131
|
+
(!page.complete && !page.frames.length))
|
|
132
|
+
throw new Error("Output recovery page did not establish coverage");
|
|
133
|
+
state.cursor = page.next;
|
|
134
|
+
state.appliedCursor = appliedCursor;
|
|
135
|
+
state.through = page.through;
|
|
136
|
+
state.coverageStart = page.coverageStart;
|
|
137
|
+
state.legacyBaseThroughEventId = page.legacyBaseThroughEventId;
|
|
138
|
+
state.pending = pending;
|
|
139
|
+
for (const event of events)
|
|
140
|
+
emit(event);
|
|
141
|
+
return page.complete;
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
state.incomplete = true;
|
|
145
|
+
state.pending = undefined;
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* UI can scope subagent rows (see commit fbe6f68 — task_update must be isolated
|
|
16
16
|
* by agentId so a subagent never clobbers the main list).
|
|
17
17
|
*/
|
|
18
|
+
import { projectOutputUserEvent } from "./userMessageDisplay.js";
|
|
18
19
|
/** Strip <system-reminder>…</system-reminder> blocks from tool output before
|
|
19
20
|
* rendering — they're context for the model, not status for the user. */
|
|
20
21
|
function stripSystemReminders(s) {
|
|
@@ -54,6 +55,7 @@ function runStateForReason(reason) {
|
|
|
54
55
|
case "model_error":
|
|
55
56
|
case "image_error":
|
|
56
57
|
case "replay_incomplete":
|
|
58
|
+
case "unverified_write":
|
|
57
59
|
return "error";
|
|
58
60
|
// "completed", "max_turns", "goal_budget_exhausted", "stop_hook_prevented",
|
|
59
61
|
// "hook_stopped", and any future/unknown terminal reason: a normal stop.
|
|
@@ -86,7 +88,8 @@ export function reduceStream(state, raw) {
|
|
|
86
88
|
const unwrapped = asStreamEvent(raw);
|
|
87
89
|
if (!unwrapped)
|
|
88
90
|
return state;
|
|
89
|
-
const {
|
|
91
|
+
const { sessionId } = unwrapped;
|
|
92
|
+
const event = projectOutputUserEvent(unwrapped.event);
|
|
90
93
|
const type = event.type;
|
|
91
94
|
let s = state;
|
|
92
95
|
if (sessionId && sessionId !== s.sessionId) {
|
|
@@ -502,7 +505,15 @@ export function reduceStream(state, raw) {
|
|
|
502
505
|
return { ...s, title: event.title };
|
|
503
506
|
}
|
|
504
507
|
case "session_user_message":
|
|
508
|
+
case "steer_injected":
|
|
505
509
|
case "user_message": {
|
|
510
|
+
if (event.agentId)
|
|
511
|
+
return s;
|
|
512
|
+
if (event.injected === true ||
|
|
513
|
+
event.authority === "agent" ||
|
|
514
|
+
event.authority === "system" ||
|
|
515
|
+
event.authority === "policy")
|
|
516
|
+
return s;
|
|
506
517
|
// History replay surfaces past user turns as a synthetic event so the
|
|
507
518
|
// same reducer rebuilds the full conversation (the live path uses
|
|
508
519
|
// appendUserMessage instead, since the phone echoes locally).
|
|
@@ -3,16 +3,11 @@
|
|
|
3
3
|
* stream events used by the live reducer. Keep Node/Electron readers outside
|
|
4
4
|
* this module: the Hub, phone and other browser hosts can share this boundary.
|
|
5
5
|
*/
|
|
6
|
-
import { type ChatState
|
|
6
|
+
import { type ChatState } from "./streamReducer.js";
|
|
7
|
+
export { transcriptUserDisplay, projectOutputUserEvent } from "./userMessageDisplay.js";
|
|
7
8
|
type ObjectValue = Record<string, unknown>;
|
|
8
|
-
/** Hide engine attachment plumbing while retaining attachment-only messages. */
|
|
9
|
-
export declare function transcriptUserDisplay(data: ObjectValue): {
|
|
10
|
-
text: string;
|
|
11
|
-
attachments: UserAttachmentSummary[];
|
|
12
|
-
};
|
|
13
9
|
/** Durable logs contain both assistant tool_use blocks and dedicated tool_use
|
|
14
10
|
* events. Their tool IDs are identical: emit idempotent starts, never two cards.
|
|
15
11
|
*/
|
|
16
12
|
export declare function transcriptToStreamEvents(records: readonly unknown[]): ObjectValue[];
|
|
17
13
|
export declare function replayTranscript(records: readonly unknown[]): ChatState;
|
|
18
|
-
export {};
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
* stream events used by the live reducer. Keep Node/Electron readers outside
|
|
4
4
|
* this module: the Hub, phone and other browser hosts can share this boundary.
|
|
5
5
|
*/
|
|
6
|
-
import { initialChatState, reduceStream
|
|
6
|
+
import { initialChatState, reduceStream } from "./streamReducer.js";
|
|
7
|
+
import { transcriptUserDisplay } from "./userMessageDisplay.js";
|
|
8
|
+
export { transcriptUserDisplay, projectOutputUserEvent } from "./userMessageDisplay.js";
|
|
7
9
|
function object(value) {
|
|
8
10
|
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
9
11
|
? value
|
|
@@ -25,90 +27,6 @@ function textContent(value) {
|
|
|
25
27
|
.map((block) => text(block.text))
|
|
26
28
|
.join("");
|
|
27
29
|
}
|
|
28
|
-
function unescapeAttribute(value) {
|
|
29
|
-
return value
|
|
30
|
-
.replace(/"/g, '"')
|
|
31
|
-
.replace(/</g, "<")
|
|
32
|
-
.replace(/>/g, ">")
|
|
33
|
-
.replace(/&/g, "&");
|
|
34
|
-
}
|
|
35
|
-
function attachmentName(path) {
|
|
36
|
-
const name = path.replace(/\\/g, "/").split("/").filter(Boolean).pop() || "附件";
|
|
37
|
-
// The canonical staging service prefixes a content hash; it is not part of
|
|
38
|
-
// the filename the user selected. Other paths keep their original basename.
|
|
39
|
-
return path.includes(".code-shell/attachments/") ? name.replace(/^[a-f0-9]{16}-/, "") : name;
|
|
40
|
-
}
|
|
41
|
-
function byteSize(value) {
|
|
42
|
-
const number = typeof value === "number" ? value : Number(value);
|
|
43
|
-
return Number.isSafeInteger(number) && number >= 0 ? number : 0;
|
|
44
|
-
}
|
|
45
|
-
/** Hide engine attachment plumbing while retaining attachment-only messages. */
|
|
46
|
-
export function transcriptUserDisplay(data) {
|
|
47
|
-
const attachments = [];
|
|
48
|
-
const imagePaths = [];
|
|
49
|
-
let raw = textContent(data.content);
|
|
50
|
-
raw = raw.replace(/<attached-file\s+path="([^"]*)">\s*([\s\S]*?)<\/attached-file>/g, (match, path, metadata) => {
|
|
51
|
-
if (!/^absolutePath:\s*.+$/m.test(metadata) || !/^origin:\s*.+$/m.test(metadata)) {
|
|
52
|
-
return match;
|
|
53
|
-
}
|
|
54
|
-
const mime = metadata.match(/^mime:\s*(.+)$/m)?.[1]?.trim();
|
|
55
|
-
attachments.push({
|
|
56
|
-
name: attachmentName(unescapeAttribute(path)),
|
|
57
|
-
path: unescapeAttribute(path),
|
|
58
|
-
size: byteSize(metadata.match(/^size:\s*(\d+)\s*$/m)?.[1]),
|
|
59
|
-
...(mime ? { mime } : {}),
|
|
60
|
-
});
|
|
61
|
-
return "";
|
|
62
|
-
});
|
|
63
|
-
raw = raw.replace(/<attached-directory\s+path="([^"]*)"[^>]*>[\s\S]*?<\/attached-directory>/g, (_match, path) => {
|
|
64
|
-
attachments.push({
|
|
65
|
-
name: `${attachmentName(unescapeAttribute(path))}/`,
|
|
66
|
-
path: unescapeAttribute(path),
|
|
67
|
-
size: 0,
|
|
68
|
-
});
|
|
69
|
-
return "";
|
|
70
|
-
});
|
|
71
|
-
raw = raw.replace(/<attached-image-paths>\s*([\s\S]*?)<\/attached-image-paths>\s*(?:\(上面附带的图片在工作区的真实路径[^\n]*\))?/g, (_match, paths) => {
|
|
72
|
-
imagePaths.push(...paths
|
|
73
|
-
.split("\n")
|
|
74
|
-
.map((path) => path.trim())
|
|
75
|
-
.filter(Boolean));
|
|
76
|
-
return "";
|
|
77
|
-
});
|
|
78
|
-
const imageBlocks = contentBlocks(data.content).filter((block) => block.type === "image");
|
|
79
|
-
for (let index = 0; index < Math.max(imageBlocks.length, imagePaths.length); index++) {
|
|
80
|
-
const source = object(imageBlocks[index]?.source);
|
|
81
|
-
const encoded = text(source?.data).replace(/\s/g, "");
|
|
82
|
-
attachments.push({
|
|
83
|
-
name: imagePaths[index] ? attachmentName(imagePaths[index]) : `图片 ${index + 1}`,
|
|
84
|
-
...(imagePaths[index] ? { path: imagePaths[index] } : {}),
|
|
85
|
-
size: encoded
|
|
86
|
-
? Math.max(0, Math.floor((encoded.length * 3) / 4) - (encoded.match(/=+$/)?.[0].length ?? 0))
|
|
87
|
-
: 0,
|
|
88
|
-
...(typeof source?.media_type === "string" ? { mime: source.media_type } : {}),
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
const explicit = Array.isArray(data.attachments)
|
|
92
|
-
? data.attachments.flatMap((item) => {
|
|
93
|
-
const attachment = object(item);
|
|
94
|
-
const name = text(attachment?.name) || text(attachment?.originalName);
|
|
95
|
-
return name
|
|
96
|
-
? [
|
|
97
|
-
{
|
|
98
|
-
name,
|
|
99
|
-
size: byteSize(attachment?.size),
|
|
100
|
-
...(typeof attachment?.mime === "string" ? { mime: attachment.mime } : {}),
|
|
101
|
-
...(typeof attachment?.path === "string" ? { path: attachment.path } : {}),
|
|
102
|
-
},
|
|
103
|
-
]
|
|
104
|
-
: [];
|
|
105
|
-
})
|
|
106
|
-
: [];
|
|
107
|
-
return {
|
|
108
|
-
text: typeof data.displayText === "string" ? data.displayText : raw.trim(),
|
|
109
|
-
attachments: explicit.length ? explicit : attachments,
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
30
|
function eventData(event) {
|
|
113
31
|
return object(event.data) ?? object(event.message) ?? event;
|
|
114
32
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Shared browser-only user input projection for live and durable replay. */
|
|
2
|
+
import type { UserAttachmentSummary } from "./streamReducer.js";
|
|
3
|
+
type ObjectValue = Record<string, unknown>;
|
|
4
|
+
/** Hide engine attachment plumbing while retaining attachment-only messages. */
|
|
5
|
+
export declare function transcriptUserDisplay(data: ObjectValue, options?: {
|
|
6
|
+
includeAbsolutePaths?: boolean;
|
|
7
|
+
cwd?: string;
|
|
8
|
+
}): {
|
|
9
|
+
text: string;
|
|
10
|
+
attachments: UserAttachmentSummary[];
|
|
11
|
+
};
|
|
12
|
+
/** Apply the canonical display projection without copying image payloads. */
|
|
13
|
+
export declare function projectOutputUserEvent<T extends {
|
|
14
|
+
type?: unknown;
|
|
15
|
+
transcriptMessage?: unknown;
|
|
16
|
+
}>(event: T): T;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
function object(value) {
|
|
2
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
3
|
+
? value
|
|
4
|
+
: undefined;
|
|
5
|
+
}
|
|
6
|
+
function text(value) {
|
|
7
|
+
return typeof value === "string" ? value : "";
|
|
8
|
+
}
|
|
9
|
+
function contentBlocks(value) {
|
|
10
|
+
return Array.isArray(value)
|
|
11
|
+
? value.map(object).filter((block) => block !== undefined)
|
|
12
|
+
: [];
|
|
13
|
+
}
|
|
14
|
+
function textContent(value) {
|
|
15
|
+
if (typeof value === "string")
|
|
16
|
+
return value;
|
|
17
|
+
return contentBlocks(value)
|
|
18
|
+
.filter((block) => block.type === "text")
|
|
19
|
+
.map((block) => text(block.text))
|
|
20
|
+
.join("");
|
|
21
|
+
}
|
|
22
|
+
function unescapeAttribute(value) {
|
|
23
|
+
return value
|
|
24
|
+
.replace(/"/g, '"')
|
|
25
|
+
.replace(/</g, "<")
|
|
26
|
+
.replace(/>/g, ">")
|
|
27
|
+
.replace(/&/g, "&");
|
|
28
|
+
}
|
|
29
|
+
function attachmentName(path) {
|
|
30
|
+
const name = path.replace(/\\/g, "/").split("/").filter(Boolean).pop() || "附件";
|
|
31
|
+
// The canonical staging service prefixes a content hash; it is not part of
|
|
32
|
+
// the filename the user selected. Other paths keep their original basename.
|
|
33
|
+
return path.includes(".code-shell/attachments/") ? name.replace(/^[a-f0-9]{16}-/, "") : name;
|
|
34
|
+
}
|
|
35
|
+
function byteSize(value) {
|
|
36
|
+
const number = typeof value === "number" ? value : Number(value);
|
|
37
|
+
return Number.isSafeInteger(number) && number >= 0 ? number : 0;
|
|
38
|
+
}
|
|
39
|
+
/** Hide engine attachment plumbing while retaining attachment-only messages. */
|
|
40
|
+
export function transcriptUserDisplay(data, options = {}) {
|
|
41
|
+
const attachments = [];
|
|
42
|
+
const absolutePath = (path) => /^(?:[a-zA-Z]:[\\/]|[\\/])/.test(path) || !options.cwd
|
|
43
|
+
? path
|
|
44
|
+
: `${options.cwd.replace(/[\\/]$/, "")}/${path}`;
|
|
45
|
+
const imagePaths = [];
|
|
46
|
+
let raw = textContent(data.content);
|
|
47
|
+
raw = raw.replace(/<attached-file\s+path="([^"]*)">\s*([\s\S]*?)<\/attached-file>/g, (match, path, metadata) => {
|
|
48
|
+
if (!/^absolutePath:\s*.+$/m.test(metadata) || !/^origin:\s*.+$/m.test(metadata)) {
|
|
49
|
+
return match;
|
|
50
|
+
}
|
|
51
|
+
const mime = metadata.match(/^mime:\s*(.+)$/m)?.[1]?.trim();
|
|
52
|
+
attachments.push({
|
|
53
|
+
name: attachmentName(unescapeAttribute(path)),
|
|
54
|
+
path: unescapeAttribute(path),
|
|
55
|
+
...(options.includeAbsolutePaths
|
|
56
|
+
? { absPath: metadata.match(/^absolutePath:\s*(.+)$/m)[1].trim() }
|
|
57
|
+
: {}),
|
|
58
|
+
size: byteSize(metadata.match(/^size:\s*(\d+)\s*$/m)?.[1]),
|
|
59
|
+
...(mime ? { mime } : {}),
|
|
60
|
+
});
|
|
61
|
+
return "";
|
|
62
|
+
});
|
|
63
|
+
raw = raw.replace(/<attached-directory\s+path="([^"]*)"[^>]*>[\s\S]*?<\/attached-directory>/g, (_match, path) => {
|
|
64
|
+
attachments.push({
|
|
65
|
+
name: `${attachmentName(unescapeAttribute(path))}/`,
|
|
66
|
+
path: unescapeAttribute(path),
|
|
67
|
+
...(options.includeAbsolutePaths ? { absPath: absolutePath(unescapeAttribute(path)) } : {}),
|
|
68
|
+
size: 0,
|
|
69
|
+
});
|
|
70
|
+
return "";
|
|
71
|
+
});
|
|
72
|
+
raw = raw.replace(/<attached-image-paths>\s*([\s\S]*?)<\/attached-image-paths>\s*(?:\(上面附带的图片在工作区的真实路径[^\n]*\))?/g, (_match, paths) => {
|
|
73
|
+
imagePaths.push(...paths
|
|
74
|
+
.split("\n")
|
|
75
|
+
.map((path) => path.trim())
|
|
76
|
+
.filter(Boolean));
|
|
77
|
+
return "";
|
|
78
|
+
});
|
|
79
|
+
const imageBlocks = contentBlocks(data.content).filter((block) => block.type === "image");
|
|
80
|
+
for (let index = 0; index < Math.max(imageBlocks.length, imagePaths.length); index++) {
|
|
81
|
+
const source = object(imageBlocks[index]?.source);
|
|
82
|
+
const encoded = text(source?.data).replace(/\s/g, "");
|
|
83
|
+
attachments.push({
|
|
84
|
+
name: imagePaths[index] ? attachmentName(imagePaths[index]) : `图片 ${index + 1}`,
|
|
85
|
+
...(imagePaths[index] ? { path: imagePaths[index] } : {}),
|
|
86
|
+
...(options.includeAbsolutePaths && imagePaths[index]
|
|
87
|
+
? { absPath: absolutePath(imagePaths[index]) }
|
|
88
|
+
: {}),
|
|
89
|
+
size: encoded
|
|
90
|
+
? Math.max(0, Math.floor((encoded.length * 3) / 4) - (encoded.match(/=+$/)?.[0].length ?? 0))
|
|
91
|
+
: byteSize(source?.byteLength),
|
|
92
|
+
...(typeof source?.media_type === "string" ? { mime: source.media_type } : {}),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
const explicit = Array.isArray(data.attachments)
|
|
96
|
+
? data.attachments.flatMap((item) => {
|
|
97
|
+
const attachment = object(item);
|
|
98
|
+
const name = text(attachment?.name) || text(attachment?.originalName);
|
|
99
|
+
return name
|
|
100
|
+
? [
|
|
101
|
+
{
|
|
102
|
+
name,
|
|
103
|
+
size: byteSize(attachment?.size),
|
|
104
|
+
...(typeof attachment?.mime === "string" ? { mime: attachment.mime } : {}),
|
|
105
|
+
...(typeof attachment?.path === "string" ? { path: attachment.path } : {}),
|
|
106
|
+
},
|
|
107
|
+
]
|
|
108
|
+
: [];
|
|
109
|
+
})
|
|
110
|
+
: [];
|
|
111
|
+
return {
|
|
112
|
+
text: typeof data.displayText === "string" ? data.displayText : raw.trim(),
|
|
113
|
+
attachments: explicit.length ? explicit : attachments,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/** Apply the canonical display projection without copying image payloads. */
|
|
117
|
+
export function projectOutputUserEvent(event) {
|
|
118
|
+
if (event.type !== "session_user_message" && event.type !== "steer_injected")
|
|
119
|
+
return event;
|
|
120
|
+
const message = object(event.transcriptMessage);
|
|
121
|
+
return message
|
|
122
|
+
? {
|
|
123
|
+
...event,
|
|
124
|
+
...transcriptUserDisplay(message, {
|
|
125
|
+
includeAbsolutePaths: true,
|
|
126
|
+
...(typeof message.cwd === "string" ? { cwd: message.cwd } : {}),
|
|
127
|
+
}),
|
|
128
|
+
}
|
|
129
|
+
: event;
|
|
130
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { LinkAuthorization, LinkAuthorizationResponse } from "@cjhyy/code-shell-link";
|
|
3
|
+
export interface LinkAuthorizationLabels {
|
|
4
|
+
openPage: string;
|
|
5
|
+
waitingBrowser: string;
|
|
6
|
+
deviceInstruction: string;
|
|
7
|
+
copy: string;
|
|
8
|
+
submit: string;
|
|
9
|
+
verifyCode: string;
|
|
10
|
+
secondFactor: string;
|
|
11
|
+
detectSession: string;
|
|
12
|
+
bindSession: string;
|
|
13
|
+
loginSession: string;
|
|
14
|
+
notInstalled: string;
|
|
15
|
+
notAuthenticated: string;
|
|
16
|
+
authenticated: string;
|
|
17
|
+
processing: string;
|
|
18
|
+
connected: string;
|
|
19
|
+
cancelled: string;
|
|
20
|
+
failed: string;
|
|
21
|
+
expired: string;
|
|
22
|
+
unsupported: string;
|
|
23
|
+
invalidUrl: string;
|
|
24
|
+
scan: string;
|
|
25
|
+
confirmOnPhone: string;
|
|
26
|
+
qrExpired: string;
|
|
27
|
+
refreshQr: string;
|
|
28
|
+
qrError: string;
|
|
29
|
+
qrAlt: string;
|
|
30
|
+
confirm: string;
|
|
31
|
+
resourcesTruncated: string;
|
|
32
|
+
expiresAt: string;
|
|
33
|
+
rejected: string;
|
|
34
|
+
cliUnavailable: string;
|
|
35
|
+
}
|
|
36
|
+
export interface LinkAuthorizationStepViewProps {
|
|
37
|
+
authorization: LinkAuthorization;
|
|
38
|
+
busy?: boolean;
|
|
39
|
+
onRespond: (response: LinkAuthorizationResponse) => void;
|
|
40
|
+
/** The host decides how to open an already validated URL. */
|
|
41
|
+
onOpenUrl: (url: string) => void;
|
|
42
|
+
/** Native remote authorization already has its own controlled window. */
|
|
43
|
+
canOpenUrl?: boolean;
|
|
44
|
+
onCopy?: (value: string) => void;
|
|
45
|
+
labels?: Partial<LinkAuthorizationLabels>;
|
|
46
|
+
}
|
|
47
|
+
/** Fixed React controls only: provider metadata never supplies HTML or handlers. */
|
|
48
|
+
export declare function LinkAuthorizationStepView(props: LinkAuthorizationStepViewProps): React.JSX.Element;
|