tinker-agent 1.0.65 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/LICENSE +201 -0
- package/NOTICE +6 -0
- package/README.md +90 -9
- package/bin/tinker.js +39 -0
- package/node_modules/ansi-regex/index.d.ts +33 -0
- package/node_modules/ansi-regex/index.js +14 -0
- package/node_modules/ansi-regex/license +9 -0
- package/node_modules/ansi-regex/package.json +61 -0
- package/node_modules/ansi-regex/readme.md +66 -0
- package/node_modules/ansi-styles/index.d.ts +236 -0
- package/node_modules/ansi-styles/index.js +223 -0
- package/node_modules/ansi-styles/license +9 -0
- package/node_modules/ansi-styles/package.json +54 -0
- package/node_modules/ansi-styles/readme.md +173 -0
- package/node_modules/chalk/license +9 -0
- package/node_modules/chalk/package.json +83 -0
- package/node_modules/chalk/readme.md +297 -0
- package/node_modules/chalk/source/index.d.ts +325 -0
- package/node_modules/chalk/source/index.js +225 -0
- package/node_modules/chalk/source/utilities.js +33 -0
- package/node_modules/chalk/source/vendor/ansi-styles/index.d.ts +236 -0
- package/node_modules/chalk/source/vendor/ansi-styles/index.js +223 -0
- package/node_modules/chalk/source/vendor/supports-color/browser.d.ts +1 -0
- package/node_modules/chalk/source/vendor/supports-color/browser.js +34 -0
- package/node_modules/chalk/source/vendor/supports-color/index.d.ts +55 -0
- package/node_modules/chalk/source/vendor/supports-color/index.js +190 -0
- package/node_modules/character-entities/index.d.ts +6 -0
- package/node_modules/character-entities/index.js +2132 -0
- package/node_modules/character-entities/license +22 -0
- package/node_modules/character-entities/package.json +78 -0
- package/node_modules/character-entities/readme.md +152 -0
- package/node_modules/decode-named-character-reference/index.d.ts +13 -0
- package/node_modules/decode-named-character-reference/index.d.ts.map +1 -0
- package/node_modules/decode-named-character-reference/index.dom.d.ts +6 -0
- package/node_modules/decode-named-character-reference/index.dom.d.ts.map +1 -0
- package/node_modules/decode-named-character-reference/index.dom.js +32 -0
- package/node_modules/decode-named-character-reference/index.js +19 -0
- package/node_modules/decode-named-character-reference/license +22 -0
- package/node_modules/decode-named-character-reference/package.json +90 -0
- package/node_modules/decode-named-character-reference/readme.md +136 -0
- package/node_modules/get-east-asian-width/index.d.ts +60 -0
- package/node_modules/get-east-asian-width/index.js +30 -0
- package/node_modules/get-east-asian-width/license +9 -0
- package/node_modules/get-east-asian-width/lookup-data.js +21 -0
- package/node_modules/get-east-asian-width/lookup.js +138 -0
- package/node_modules/get-east-asian-width/package.json +71 -0
- package/node_modules/get-east-asian-width/readme.md +65 -0
- package/node_modules/get-east-asian-width/utilities.js +24 -0
- package/node_modules/has-flag/index.d.ts +39 -0
- package/node_modules/has-flag/index.js +8 -0
- package/node_modules/has-flag/license +9 -0
- package/node_modules/has-flag/package.json +49 -0
- package/node_modules/has-flag/readme.md +74 -0
- package/node_modules/is-fullwidth-code-point/index.d.ts +17 -0
- package/node_modules/is-fullwidth-code-point/index.js +12 -0
- package/node_modules/is-fullwidth-code-point/license +9 -0
- package/node_modules/is-fullwidth-code-point/package.json +53 -0
- package/node_modules/is-fullwidth-code-point/readme.md +31 -0
- package/node_modules/markdansi/LICENSE +21 -0
- package/node_modules/markdansi/README.md +167 -0
- package/node_modules/markdansi/dist/ast.d.ts +99 -0
- package/node_modules/markdansi/dist/ast.js +1 -0
- package/node_modules/markdansi/dist/cli.d.ts +17 -0
- package/node_modules/markdansi/dist/cli.js +208 -0
- package/node_modules/markdansi/dist/hyperlink.d.ts +9 -0
- package/node_modules/markdansi/dist/hyperlink.js +26 -0
- package/node_modules/markdansi/dist/index.d.ts +10 -0
- package/node_modules/markdansi/dist/index.js +16 -0
- package/node_modules/markdansi/dist/parser.d.ts +2 -0
- package/node_modules/markdansi/dist/parser.js +205 -0
- package/node_modules/markdansi/dist/render.d.ts +9 -0
- package/node_modules/markdansi/dist/render.js +680 -0
- package/node_modules/markdansi/dist/stream.d.ts +39 -0
- package/node_modules/markdansi/dist/stream.js +192 -0
- package/node_modules/markdansi/dist/theme.d.ts +18 -0
- package/node_modules/markdansi/dist/theme.js +105 -0
- package/node_modules/markdansi/dist/types.d.ts +58 -0
- package/node_modules/markdansi/dist/types.js +1 -0
- package/node_modules/markdansi/dist/wrap.d.ts +10 -0
- package/node_modules/markdansi/dist/wrap.js +73 -0
- package/node_modules/markdansi/docs/spec.md +110 -0
- package/node_modules/markdansi/package.json +78 -0
- package/node_modules/markdansi/tsconfig.json +18 -0
- package/node_modules/marked/LICENSE +44 -0
- package/node_modules/marked/README.md +115 -0
- package/node_modules/marked/bin/main.js +284 -0
- package/node_modules/marked/bin/marked.js +16 -0
- package/node_modules/marked/lib/marked.d.ts +760 -0
- package/node_modules/marked/lib/marked.esm.js +77 -0
- package/node_modules/marked/lib/marked.esm.js.map +7 -0
- package/node_modules/marked/lib/marked.umd.js +79 -0
- package/node_modules/marked/lib/marked.umd.js.map +7 -0
- package/node_modules/marked/man/marked.1 +113 -0
- package/node_modules/marked/man/marked.1.md +93 -0
- package/node_modules/marked/package.json +103 -0
- package/node_modules/slice-ansi/index.d.ts +19 -0
- package/node_modules/slice-ansi/index.js +317 -0
- package/node_modules/slice-ansi/license +10 -0
- package/node_modules/slice-ansi/package.json +59 -0
- package/node_modules/slice-ansi/readme.md +55 -0
- package/node_modules/slice-ansi/tokenize-ansi.js +752 -0
- package/node_modules/string-width/index.d.ts +39 -0
- package/node_modules/string-width/index.js +203 -0
- package/node_modules/string-width/license +9 -0
- package/node_modules/string-width/package.json +65 -0
- package/node_modules/string-width/readme.md +66 -0
- package/node_modules/strip-ansi/index.d.ts +15 -0
- package/node_modules/strip-ansi/index.js +19 -0
- package/node_modules/strip-ansi/license +9 -0
- package/node_modules/strip-ansi/package.json +59 -0
- package/node_modules/strip-ansi/readme.md +37 -0
- package/node_modules/supports-color/browser.d.ts +1 -0
- package/node_modules/supports-color/browser.js +35 -0
- package/node_modules/supports-color/index.d.ts +55 -0
- package/node_modules/supports-color/index.js +202 -0
- package/node_modules/supports-color/license +9 -0
- package/node_modules/supports-color/package.json +64 -0
- package/node_modules/supports-color/readme.md +75 -0
- package/node_modules/supports-hyperlinks/browser.js +10 -0
- package/node_modules/supports-hyperlinks/index.d.ts +25 -0
- package/node_modules/supports-hyperlinks/index.js +163 -0
- package/node_modules/supports-hyperlinks/license +10 -0
- package/node_modules/supports-hyperlinks/package.json +47 -0
- package/node_modules/supports-hyperlinks/readme.md +45 -0
- package/package.json +41 -10
- package/patches/markdansi@0.3.2.patch +22 -2
- package/src/agent/context-builder.ts +24 -10
- package/src/agent/context-meter.ts +89 -5
- package/src/agent/loop.ts +107 -10
- package/src/agent/runtime-session.ts +1437 -82
- package/src/agent/session-ledger.ts +169 -29
- package/src/agent/types.ts +12 -1
- package/src/agent/user-prompt-projection.ts +62 -0
- package/src/cli/config.ts +57 -14
- package/src/cli/model-profiles.ts +124 -0
- package/src/cli/run-runner.ts +4 -1
- package/src/cli/tui-runner.tsx +68 -30
- package/src/context/compiled-context-hash.ts +1 -1
- package/src/context/compiled-context-validator.ts +92 -15
- package/src/context/context-automation-policy.ts +121 -0
- package/src/context/context-manager.ts +390 -11
- package/src/context/context-policy.ts +8 -0
- package/src/context/context-protocol-validator.ts +11 -1
- package/src/context/context-revision-compiler.ts +190 -27
- package/src/context/context-revision.ts +105 -19
- package/src/context/context-surface.ts +329 -0
- package/src/context/prefix-retirement-planner.ts +570 -0
- package/src/context/protocol-frame.ts +30 -3
- package/src/context/recall-retirement-contract.ts +27 -0
- package/src/context/swap-planner.ts +34 -10
- package/src/events/observation-text-log.ts +3 -3
- package/src/events/stdout-event-printer.ts +39 -3
- package/src/events/types.ts +172 -35
- package/src/ids/runtime-id.ts +6 -0
- package/src/ids/uuid-v7.ts +7 -0
- package/src/image/image-asset-store.ts +319 -0
- package/src/image/image-input-policy.ts +14 -0
- package/src/image/image-probe.ts +233 -0
- package/src/image/image-types.ts +262 -0
- package/src/instructions/project-instructions.ts +4 -1
- package/src/mcp/mcp-manager.ts +83 -19
- package/src/model/fake-model-client.ts +9 -1
- package/src/model/input-token-estimator.ts +25 -0
- package/src/model/model-client.ts +60 -0
- package/src/model/model-request-preflight.ts +1 -0
- package/src/model/moonshot-input-token-estimator.ts +107 -0
- package/src/model/openai-chat-mapping.ts +77 -1
- package/src/model/openai-chat-model-client.ts +389 -59
- package/src/model/openai-chat-stream.ts +272 -0
- package/src/model/prompt-prefix-hash.ts +2 -4
- package/src/model/token-estimator.ts +4 -0
- package/src/observation/observation-builder.ts +29 -0
- package/src/session/resume-projection.ts +94 -34
- package/src/session/session-catalog.ts +4 -4
- package/src/session/session-errors.ts +1 -1
- package/src/session/session-history-reader.ts +151 -4
- package/src/session/session-last-response-reader.ts +189 -0
- package/src/session/session-schema.ts +349 -35
- package/src/session/session-store.ts +3266 -255
- package/src/session/sqlite-session-ledger.ts +9 -4
- package/src/skills/skill-catalog.ts +182 -0
- package/src/skills/skill-context.ts +364 -0
- package/src/skills/skill-loader.ts +779 -0
- package/src/skills/skill-tool.ts +297 -0
- package/src/tools/edit.ts +51 -64
- package/src/tools/ensure-parent-directory.ts +6 -0
- package/src/tools/read.ts +2 -2
- package/src/tools/recall.ts +43 -40
- package/src/tools/registry.ts +24 -3
- package/src/tools/ripgrep.ts +3 -2
- package/src/tools/types.ts +43 -4
- package/src/tools/write.ts +20 -31
- package/src/tui/app.tsx +333 -96
- package/src/tui/clipboard.ts +5 -0
- package/src/tui/components/mcp-panel.tsx +28 -0
- package/src/tui/components/prompt-input.tsx +790 -257
- package/src/tui/components/skills-panel.tsx +31 -0
- package/src/tui/components/timeline.tsx +30 -1
- package/src/tui/event-store.ts +85 -2
- package/src/tui/file-mention.ts +234 -0
- package/src/tui/line-editor.ts +1 -0
- package/src/tui/project-slash-commands.ts +229 -0
- package/src/tui/prompt-draft.ts +510 -0
- package/src/tui/prompt-history.ts +294 -22
- package/src/tui/slash-commands.ts +46 -2
- package/src/tui/tui-session-controller.ts +110 -48
- package/src/tui/workspace-file-search.ts +94 -0
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
import type { ImportedImageAsset } from "../image/image-asset-store";
|
|
2
|
+
import {
|
|
3
|
+
imageLabel,
|
|
4
|
+
validateImageAssetRef,
|
|
5
|
+
validateOriginalImageName,
|
|
6
|
+
validateUserMessage,
|
|
7
|
+
type CodePointRange,
|
|
8
|
+
type ImageAttachmentId,
|
|
9
|
+
type ImageAssetRef,
|
|
10
|
+
type UserMessage,
|
|
11
|
+
} from "../image/image-types";
|
|
12
|
+
import { IMAGE_INPUT_POLICY } from "../image/image-input-policy";
|
|
13
|
+
import {
|
|
14
|
+
backspace,
|
|
15
|
+
createLineEditorState,
|
|
16
|
+
deleteForward,
|
|
17
|
+
deleteToLineStart,
|
|
18
|
+
insert,
|
|
19
|
+
moveDown,
|
|
20
|
+
moveLeft,
|
|
21
|
+
moveRight,
|
|
22
|
+
moveToLineEnd,
|
|
23
|
+
moveToLineStart,
|
|
24
|
+
moveUp,
|
|
25
|
+
type LineEditorState,
|
|
26
|
+
} from "./line-editor";
|
|
27
|
+
|
|
28
|
+
export type PromptElement = {
|
|
29
|
+
readonly kind: "image";
|
|
30
|
+
readonly attachmentId: ImageAttachmentId;
|
|
31
|
+
readonly label: string;
|
|
32
|
+
readonly range: CodePointRange;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type DraftImageAttachment = {
|
|
36
|
+
readonly attachmentId: ImageAttachmentId;
|
|
37
|
+
readonly asset: ImageAssetRef;
|
|
38
|
+
readonly originalName: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type PromptDraft = {
|
|
42
|
+
readonly editor: LineEditorState;
|
|
43
|
+
readonly elements: readonly PromptElement[];
|
|
44
|
+
readonly attachments: readonly DraftImageAttachment[];
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export function createPromptDraft(value = ""): PromptDraft {
|
|
48
|
+
return Object.freeze({
|
|
49
|
+
editor: Object.freeze(createLineEditorState(value)),
|
|
50
|
+
elements: Object.freeze([]),
|
|
51
|
+
attachments: Object.freeze([]),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function validatePromptDraft(draft: PromptDraft): void {
|
|
56
|
+
const chars = [...draft.editor.value];
|
|
57
|
+
if (
|
|
58
|
+
!Number.isSafeInteger(draft.editor.cursor) ||
|
|
59
|
+
draft.editor.cursor < 0 ||
|
|
60
|
+
draft.editor.cursor > chars.length
|
|
61
|
+
) {
|
|
62
|
+
throw new Error("Prompt draft cursor is outside the text.");
|
|
63
|
+
}
|
|
64
|
+
if (
|
|
65
|
+
draft.editor.preferredColumn !== undefined &&
|
|
66
|
+
(!Number.isSafeInteger(draft.editor.preferredColumn) ||
|
|
67
|
+
draft.editor.preferredColumn < 0)
|
|
68
|
+
) {
|
|
69
|
+
throw new Error("Prompt draft preferred column is invalid.");
|
|
70
|
+
}
|
|
71
|
+
if (draft.elements.length !== draft.attachments.length) {
|
|
72
|
+
throw new Error("Prompt draft image elements and attachments are unbalanced.");
|
|
73
|
+
}
|
|
74
|
+
if (draft.elements.length > IMAGE_INPUT_POLICY.maxImagesPerMessage) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
`Prompt has ${draft.elements.length} images; maximum is ${IMAGE_INPUT_POLICY.maxImagesPerMessage}.`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
const attachments = new Map(
|
|
80
|
+
draft.attachments.map((attachment) => [attachment.attachmentId, attachment]),
|
|
81
|
+
);
|
|
82
|
+
if (attachments.size !== draft.attachments.length) {
|
|
83
|
+
throw new Error("Prompt draft attachment IDs must be unique.");
|
|
84
|
+
}
|
|
85
|
+
let previousEnd = -1;
|
|
86
|
+
for (let index = 0; index < draft.elements.length; index += 1) {
|
|
87
|
+
const element = draft.elements[index];
|
|
88
|
+
const attachment = attachments.get(element.attachmentId);
|
|
89
|
+
if (attachment === undefined) {
|
|
90
|
+
throw new Error("Prompt image element has no attachment.");
|
|
91
|
+
}
|
|
92
|
+
if (element.label !== imageLabel(index + 1)) {
|
|
93
|
+
throw new Error("Prompt image labels are not continuously numbered.");
|
|
94
|
+
}
|
|
95
|
+
if (
|
|
96
|
+
element.range.start < previousEnd ||
|
|
97
|
+
element.range.start < 0 ||
|
|
98
|
+
element.range.end <= element.range.start ||
|
|
99
|
+
element.range.end > chars.length ||
|
|
100
|
+
chars.slice(element.range.start, element.range.end).join("") !== element.label
|
|
101
|
+
) {
|
|
102
|
+
throw new Error("Prompt image element range is invalid.");
|
|
103
|
+
}
|
|
104
|
+
validateImageAssetRef(attachment.asset);
|
|
105
|
+
validateOriginalImageName(attachment.originalName);
|
|
106
|
+
attachments.delete(element.attachmentId);
|
|
107
|
+
previousEnd = element.range.end;
|
|
108
|
+
}
|
|
109
|
+
if (attachments.size !== 0) {
|
|
110
|
+
throw new Error("Prompt image attachment has no element.");
|
|
111
|
+
}
|
|
112
|
+
if (draft.elements.length > 0) {
|
|
113
|
+
validateUserMessage(draftToUserMessage(draft));
|
|
114
|
+
}
|
|
115
|
+
if (
|
|
116
|
+
draft.elements.some(
|
|
117
|
+
(element) =>
|
|
118
|
+
element.range.start < draft.editor.cursor &&
|
|
119
|
+
draft.editor.cursor < element.range.end,
|
|
120
|
+
)
|
|
121
|
+
) {
|
|
122
|
+
throw new Error("Prompt draft cursor is inside an atomic image element.");
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function insertDraftImage(
|
|
127
|
+
draft: PromptDraft,
|
|
128
|
+
input: {
|
|
129
|
+
replace: CodePointRange;
|
|
130
|
+
attachmentId: ImageAttachmentId;
|
|
131
|
+
imported: ImportedImageAsset;
|
|
132
|
+
},
|
|
133
|
+
): PromptDraft {
|
|
134
|
+
validatePromptDraft(draft);
|
|
135
|
+
if (draft.attachments.length >= IMAGE_INPUT_POLICY.maxImagesPerMessage) {
|
|
136
|
+
throw new Error(
|
|
137
|
+
`A prompt can contain at most ${IMAGE_INPUT_POLICY.maxImagesPerMessage} images.`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
if (
|
|
141
|
+
input.replace.start < 0 ||
|
|
142
|
+
input.replace.end < input.replace.start ||
|
|
143
|
+
input.replace.end > [...draft.editor.value].length
|
|
144
|
+
) {
|
|
145
|
+
throw new Error("Image insertion range is invalid.");
|
|
146
|
+
}
|
|
147
|
+
const provisionalLabel = imageLabel(draft.elements.length + 1);
|
|
148
|
+
const suffix = [...draft.editor.value].slice(input.replace.end);
|
|
149
|
+
const separator = suffix[0] === " " || suffix[0] === "\t" ? "" : " ";
|
|
150
|
+
const edited = applyDraftEdit(
|
|
151
|
+
draft,
|
|
152
|
+
input.replace,
|
|
153
|
+
`${provisionalLabel}${separator}`,
|
|
154
|
+
);
|
|
155
|
+
const insertedRange = Object.freeze({
|
|
156
|
+
start: input.replace.start,
|
|
157
|
+
end: input.replace.start + [...provisionalLabel].length,
|
|
158
|
+
});
|
|
159
|
+
const withImage: PromptDraft = Object.freeze({
|
|
160
|
+
editor: edited.editor,
|
|
161
|
+
elements: Object.freeze(
|
|
162
|
+
[
|
|
163
|
+
...edited.elements,
|
|
164
|
+
Object.freeze({
|
|
165
|
+
kind: "image" as const,
|
|
166
|
+
attachmentId: input.attachmentId,
|
|
167
|
+
label: provisionalLabel,
|
|
168
|
+
range: insertedRange,
|
|
169
|
+
}),
|
|
170
|
+
].sort((left, right) => left.range.start - right.range.start),
|
|
171
|
+
),
|
|
172
|
+
attachments: Object.freeze([
|
|
173
|
+
...edited.attachments,
|
|
174
|
+
Object.freeze({
|
|
175
|
+
attachmentId: input.attachmentId,
|
|
176
|
+
asset: input.imported.asset,
|
|
177
|
+
originalName: input.imported.originalName,
|
|
178
|
+
}),
|
|
179
|
+
]),
|
|
180
|
+
});
|
|
181
|
+
return renumberDraft(withImage);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function insertDraftText(draft: PromptDraft, text: string): PromptDraft {
|
|
185
|
+
return applyEditorOperation(draft, (editor) => insert(editor, text));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function replaceDraftText(
|
|
189
|
+
draft: PromptDraft,
|
|
190
|
+
range: CodePointRange,
|
|
191
|
+
replacement: string,
|
|
192
|
+
): PromptDraft {
|
|
193
|
+
validatePromptDraft(draft);
|
|
194
|
+
const length = [...draft.editor.value].length;
|
|
195
|
+
if (
|
|
196
|
+
!Number.isSafeInteger(range.start) ||
|
|
197
|
+
!Number.isSafeInteger(range.end) ||
|
|
198
|
+
range.start < 0 ||
|
|
199
|
+
range.end < range.start ||
|
|
200
|
+
range.end > length
|
|
201
|
+
) {
|
|
202
|
+
throw new Error("Prompt replacement range is invalid.");
|
|
203
|
+
}
|
|
204
|
+
return applyDraftEdit(draft, range, replacement);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function backspaceDraft(draft: PromptDraft): PromptDraft {
|
|
208
|
+
return applyEditorOperation(draft, backspace);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function deleteForwardDraft(draft: PromptDraft): PromptDraft {
|
|
212
|
+
return applyEditorOperation(draft, deleteForward);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function deleteToLineStartDraft(draft: PromptDraft): PromptDraft {
|
|
216
|
+
return applyEditorOperation(draft, deleteToLineStart);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function moveDraftLeft(draft: PromptDraft): PromptDraft {
|
|
220
|
+
const element = draft.elements.find(
|
|
221
|
+
(entry) => entry.range.end === draft.editor.cursor,
|
|
222
|
+
);
|
|
223
|
+
return withCursor(
|
|
224
|
+
draft,
|
|
225
|
+
element?.range.start ?? moveLeft(draft.editor).cursor,
|
|
226
|
+
"left",
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function moveDraftRight(draft: PromptDraft): PromptDraft {
|
|
231
|
+
const element = draft.elements.find(
|
|
232
|
+
(entry) => entry.range.start === draft.editor.cursor,
|
|
233
|
+
);
|
|
234
|
+
return withCursor(
|
|
235
|
+
draft,
|
|
236
|
+
element?.range.end ?? moveRight(draft.editor).cursor,
|
|
237
|
+
"right",
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function moveDraftUp(draft: PromptDraft): PromptDraft {
|
|
242
|
+
const moved = moveUp(draft.editor);
|
|
243
|
+
return withEditorCursor(draft, moved, "up");
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function moveDraftDown(draft: PromptDraft): PromptDraft {
|
|
247
|
+
const moved = moveDown(draft.editor);
|
|
248
|
+
return withEditorCursor(draft, moved, "down");
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function moveDraftToLineStart(draft: PromptDraft): PromptDraft {
|
|
252
|
+
return withEditorCursor(draft, moveToLineStart(draft.editor), "left");
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function moveDraftToLineEnd(draft: PromptDraft): PromptDraft {
|
|
256
|
+
return withEditorCursor(draft, moveToLineEnd(draft.editor), "right");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function draftSubmissionSnapshot(draft: PromptDraft): {
|
|
260
|
+
readonly draft: PromptDraft;
|
|
261
|
+
readonly userMessage: UserMessage;
|
|
262
|
+
} {
|
|
263
|
+
validatePromptDraft(draft);
|
|
264
|
+
const chars = [...draft.editor.value];
|
|
265
|
+
let start = 0;
|
|
266
|
+
while (start < chars.length && chars[start].trim() === "") {
|
|
267
|
+
start += 1;
|
|
268
|
+
}
|
|
269
|
+
let end = chars.length;
|
|
270
|
+
while (end > start && chars[end - 1].trim() === "") {
|
|
271
|
+
end -= 1;
|
|
272
|
+
}
|
|
273
|
+
let normalized = draft;
|
|
274
|
+
if (end < chars.length) {
|
|
275
|
+
normalized = applyDraftEdit(normalized, { start: end, end: chars.length }, "");
|
|
276
|
+
}
|
|
277
|
+
if (start > 0) {
|
|
278
|
+
normalized = applyDraftEdit(normalized, { start: 0, end: start }, "");
|
|
279
|
+
}
|
|
280
|
+
validatePromptDraft(normalized);
|
|
281
|
+
const userMessage = draftToUserMessage(normalized);
|
|
282
|
+
validateUserMessage(userMessage);
|
|
283
|
+
return Object.freeze({ draft: normalized, userMessage });
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export function draftToUserMessage(draft: PromptDraft): UserMessage {
|
|
287
|
+
const attachmentsById = new Map(
|
|
288
|
+
draft.attachments.map((attachment) => [attachment.attachmentId, attachment]),
|
|
289
|
+
);
|
|
290
|
+
return Object.freeze({
|
|
291
|
+
role: "user",
|
|
292
|
+
content: draft.editor.value,
|
|
293
|
+
...(draft.elements.length === 0
|
|
294
|
+
? {}
|
|
295
|
+
: {
|
|
296
|
+
attachments: Object.freeze(
|
|
297
|
+
draft.elements.map((element) => {
|
|
298
|
+
const attachment = attachmentsById.get(element.attachmentId);
|
|
299
|
+
if (attachment === undefined) {
|
|
300
|
+
throw new Error("Prompt image attachment disappeared.");
|
|
301
|
+
}
|
|
302
|
+
return Object.freeze({
|
|
303
|
+
attachmentId: element.attachmentId,
|
|
304
|
+
assetId: attachment.asset.assetId,
|
|
305
|
+
label: element.label,
|
|
306
|
+
range: element.range,
|
|
307
|
+
mimeType: attachment.asset.mimeType,
|
|
308
|
+
byteLength: attachment.asset.byteLength,
|
|
309
|
+
width: attachment.asset.width,
|
|
310
|
+
height: attachment.asset.height,
|
|
311
|
+
originalName: attachment.originalName,
|
|
312
|
+
});
|
|
313
|
+
}),
|
|
314
|
+
),
|
|
315
|
+
}),
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function promptDraftChanged(left: PromptDraft, right: PromptDraft): boolean {
|
|
320
|
+
return (
|
|
321
|
+
left.editor.value !== right.editor.value ||
|
|
322
|
+
left.editor.cursor !== right.editor.cursor ||
|
|
323
|
+
left.editor.preferredColumn !== right.editor.preferredColumn ||
|
|
324
|
+
left.elements !== right.elements ||
|
|
325
|
+
left.attachments !== right.attachments
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function applyEditorOperation(
|
|
330
|
+
draft: PromptDraft,
|
|
331
|
+
operation: (editor: LineEditorState) => LineEditorState,
|
|
332
|
+
): PromptDraft {
|
|
333
|
+
validatePromptDraft(draft);
|
|
334
|
+
const raw = operation(draft.editor);
|
|
335
|
+
if (raw.value === draft.editor.value) {
|
|
336
|
+
return withEditorCursor(draft, raw, "right");
|
|
337
|
+
}
|
|
338
|
+
const before = [...draft.editor.value];
|
|
339
|
+
const after = [...raw.value];
|
|
340
|
+
let prefix = 0;
|
|
341
|
+
while (prefix < before.length && before[prefix] === after[prefix]) {
|
|
342
|
+
prefix += 1;
|
|
343
|
+
}
|
|
344
|
+
let suffix = 0;
|
|
345
|
+
while (
|
|
346
|
+
suffix < before.length - prefix &&
|
|
347
|
+
suffix < after.length - prefix &&
|
|
348
|
+
before[before.length - 1 - suffix] === after[after.length - 1 - suffix]
|
|
349
|
+
) {
|
|
350
|
+
suffix += 1;
|
|
351
|
+
}
|
|
352
|
+
const replacement = after.slice(prefix, after.length - suffix).join("");
|
|
353
|
+
return applyDraftEdit(
|
|
354
|
+
draft,
|
|
355
|
+
{ start: prefix, end: before.length - suffix },
|
|
356
|
+
replacement,
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function applyDraftEdit(
|
|
361
|
+
draft: PromptDraft,
|
|
362
|
+
requested: CodePointRange,
|
|
363
|
+
replacement: string,
|
|
364
|
+
): PromptDraft {
|
|
365
|
+
const chars = [...draft.editor.value];
|
|
366
|
+
let start = requested.start;
|
|
367
|
+
let end = requested.end;
|
|
368
|
+
const isInsertion = start === end;
|
|
369
|
+
const removedIds = new Set<ImageAttachmentId>();
|
|
370
|
+
for (const element of draft.elements) {
|
|
371
|
+
const intersects = isInsertion
|
|
372
|
+
? element.range.start < start && start < element.range.end
|
|
373
|
+
: element.range.start < end && start < element.range.end;
|
|
374
|
+
if (intersects) {
|
|
375
|
+
start = Math.min(start, element.range.start);
|
|
376
|
+
end = Math.max(end, element.range.end);
|
|
377
|
+
removedIds.add(element.attachmentId);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
const inserted = [...replacement];
|
|
381
|
+
const delta = inserted.length - (end - start);
|
|
382
|
+
const value = [...chars.slice(0, start), ...inserted, ...chars.slice(end)].join("");
|
|
383
|
+
const elements = draft.elements
|
|
384
|
+
.filter((element) => !removedIds.has(element.attachmentId))
|
|
385
|
+
.map((element) =>
|
|
386
|
+
element.range.start >= end
|
|
387
|
+
? Object.freeze({
|
|
388
|
+
...element,
|
|
389
|
+
range: Object.freeze({
|
|
390
|
+
start: element.range.start + delta,
|
|
391
|
+
end: element.range.end + delta,
|
|
392
|
+
}),
|
|
393
|
+
})
|
|
394
|
+
: element,
|
|
395
|
+
);
|
|
396
|
+
const edited: PromptDraft = Object.freeze({
|
|
397
|
+
editor: Object.freeze({ value, cursor: start + inserted.length }),
|
|
398
|
+
elements: Object.freeze(elements),
|
|
399
|
+
attachments: Object.freeze(
|
|
400
|
+
draft.attachments.filter(
|
|
401
|
+
(attachment) => !removedIds.has(attachment.attachmentId),
|
|
402
|
+
),
|
|
403
|
+
),
|
|
404
|
+
});
|
|
405
|
+
return renumberDraft(edited);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function renumberDraft(draft: PromptDraft): PromptDraft {
|
|
409
|
+
const ordered = [...draft.elements].sort(
|
|
410
|
+
(left, right) => left.range.start - right.range.start,
|
|
411
|
+
);
|
|
412
|
+
const source = [...draft.editor.value];
|
|
413
|
+
const output: string[] = [];
|
|
414
|
+
const elements: PromptElement[] = [];
|
|
415
|
+
let sourceOffset = 0;
|
|
416
|
+
let outputOffset = 0;
|
|
417
|
+
let cursor = draft.editor.cursor;
|
|
418
|
+
for (let index = 0; index < ordered.length; index += 1) {
|
|
419
|
+
const element = ordered[index];
|
|
420
|
+
const prefix = source.slice(sourceOffset, element.range.start);
|
|
421
|
+
output.push(...prefix);
|
|
422
|
+
outputOffset += prefix.length;
|
|
423
|
+
const oldLength = element.range.end - element.range.start;
|
|
424
|
+
const label = imageLabel(index + 1);
|
|
425
|
+
const labelChars = [...label];
|
|
426
|
+
output.push(...labelChars);
|
|
427
|
+
elements.push(
|
|
428
|
+
Object.freeze({
|
|
429
|
+
kind: "image",
|
|
430
|
+
attachmentId: element.attachmentId,
|
|
431
|
+
label,
|
|
432
|
+
range: Object.freeze({
|
|
433
|
+
start: outputOffset,
|
|
434
|
+
end: outputOffset + labelChars.length,
|
|
435
|
+
}),
|
|
436
|
+
}),
|
|
437
|
+
);
|
|
438
|
+
const labelDelta = labelChars.length - oldLength;
|
|
439
|
+
if (cursor >= element.range.end) {
|
|
440
|
+
cursor += labelDelta;
|
|
441
|
+
}
|
|
442
|
+
outputOffset += labelChars.length;
|
|
443
|
+
sourceOffset = element.range.end;
|
|
444
|
+
}
|
|
445
|
+
output.push(...source.slice(sourceOffset));
|
|
446
|
+
const attachmentsById = new Map(
|
|
447
|
+
draft.attachments.map((attachment) => [attachment.attachmentId, attachment]),
|
|
448
|
+
);
|
|
449
|
+
const normalized: PromptDraft = Object.freeze({
|
|
450
|
+
editor: Object.freeze({ value: output.join(""), cursor }),
|
|
451
|
+
elements: Object.freeze(elements),
|
|
452
|
+
attachments: Object.freeze(
|
|
453
|
+
elements.map((element) => {
|
|
454
|
+
const attachment = attachmentsById.get(element.attachmentId);
|
|
455
|
+
if (attachment === undefined) {
|
|
456
|
+
throw new Error("Prompt attachment disappeared while renumbering.");
|
|
457
|
+
}
|
|
458
|
+
return attachment;
|
|
459
|
+
}),
|
|
460
|
+
),
|
|
461
|
+
});
|
|
462
|
+
validatePromptDraft(normalized);
|
|
463
|
+
return normalized;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function withEditorCursor(
|
|
467
|
+
draft: PromptDraft,
|
|
468
|
+
editor: LineEditorState,
|
|
469
|
+
direction: "left" | "right" | "up" | "down",
|
|
470
|
+
): PromptDraft {
|
|
471
|
+
const cursor = normalizeCursor(draft.elements, editor.cursor, direction);
|
|
472
|
+
const next = Object.freeze({
|
|
473
|
+
...draft,
|
|
474
|
+
editor: Object.freeze({ ...editor, cursor }),
|
|
475
|
+
});
|
|
476
|
+
validatePromptDraft(next);
|
|
477
|
+
return next;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function withCursor(
|
|
481
|
+
draft: PromptDraft,
|
|
482
|
+
cursor: number,
|
|
483
|
+
direction: "left" | "right" | "up" | "down",
|
|
484
|
+
): PromptDraft {
|
|
485
|
+
return withEditorCursor(draft, { value: draft.editor.value, cursor }, direction);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function normalizeCursor(
|
|
489
|
+
elements: readonly PromptElement[],
|
|
490
|
+
cursor: number,
|
|
491
|
+
direction: "left" | "right" | "up" | "down",
|
|
492
|
+
): number {
|
|
493
|
+
const element = elements.find(
|
|
494
|
+
(entry) => entry.range.start < cursor && cursor < entry.range.end,
|
|
495
|
+
);
|
|
496
|
+
if (element === undefined) {
|
|
497
|
+
return cursor;
|
|
498
|
+
}
|
|
499
|
+
const leftDistance = cursor - element.range.start;
|
|
500
|
+
const rightDistance = element.range.end - cursor;
|
|
501
|
+
if (leftDistance < rightDistance) {
|
|
502
|
+
return element.range.start;
|
|
503
|
+
}
|
|
504
|
+
if (rightDistance < leftDistance) {
|
|
505
|
+
return element.range.end;
|
|
506
|
+
}
|
|
507
|
+
return direction === "left" || direction === "up"
|
|
508
|
+
? element.range.start
|
|
509
|
+
: element.range.end;
|
|
510
|
+
}
|