reigncode-app 1.3.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/AGENTS.md +30 -0
- package/Dockerfile +21 -0
- package/README.md +51 -0
- package/bunfig.toml +3 -0
- package/create-effect-simplification-spec.md +515 -0
- package/e2e/AGENTS.md +226 -0
- package/e2e/actions.ts +1018 -0
- package/e2e/app/home.spec.ts +24 -0
- package/e2e/app/navigation.spec.ts +10 -0
- package/e2e/app/palette.spec.ts +20 -0
- package/e2e/app/server-default.spec.ts +58 -0
- package/e2e/app/session.spec.ts +16 -0
- package/e2e/app/titlebar-history.spec.ts +120 -0
- package/e2e/commands/input-focus.spec.ts +15 -0
- package/e2e/commands/panels.spec.ts +33 -0
- package/e2e/commands/tab-close.spec.ts +32 -0
- package/e2e/files/file-open.spec.ts +31 -0
- package/e2e/files/file-tree.spec.ts +56 -0
- package/e2e/files/file-viewer.spec.ts +156 -0
- package/e2e/fixtures.ts +154 -0
- package/e2e/models/model-picker.spec.ts +48 -0
- package/e2e/models/models-visibility.spec.ts +61 -0
- package/e2e/projects/project-edit.spec.ts +43 -0
- package/e2e/projects/projects-close.spec.ts +54 -0
- package/e2e/projects/projects-switch.spec.ts +116 -0
- package/e2e/projects/workspace-new-session.spec.ts +94 -0
- package/e2e/projects/workspaces.spec.ts +375 -0
- package/e2e/prompt/context.spec.ts +95 -0
- package/e2e/prompt/prompt-async.spec.ts +76 -0
- package/e2e/prompt/prompt-drop-file-uri.spec.ts +22 -0
- package/e2e/prompt/prompt-drop-file.spec.ts +30 -0
- package/e2e/prompt/prompt-history.spec.ts +184 -0
- package/e2e/prompt/prompt-mention.spec.ts +26 -0
- package/e2e/prompt/prompt-multiline.spec.ts +24 -0
- package/e2e/prompt/prompt-shell.spec.ts +62 -0
- package/e2e/prompt/prompt-slash-open.spec.ts +22 -0
- package/e2e/prompt/prompt-slash-share.spec.ts +64 -0
- package/e2e/prompt/prompt-slash-terminal.spec.ts +18 -0
- package/e2e/prompt/prompt.spec.ts +55 -0
- package/e2e/selectors.ts +75 -0
- package/e2e/session/session-child-navigation.spec.ts +37 -0
- package/e2e/session/session-composer-dock.spec.ts +530 -0
- package/e2e/session/session-model-persistence.spec.ts +359 -0
- package/e2e/session/session-review.spec.ts +426 -0
- package/e2e/session/session-undo-redo.spec.ts +233 -0
- package/e2e/session/session.spec.ts +174 -0
- package/e2e/settings/settings-keybinds.spec.ts +389 -0
- package/e2e/settings/settings-models.spec.ts +122 -0
- package/e2e/settings/settings-providers.spec.ts +136 -0
- package/e2e/settings/settings.spec.ts +519 -0
- package/e2e/sidebar/sidebar-popover-actions.spec.ts +118 -0
- package/e2e/sidebar/sidebar-session-links.spec.ts +30 -0
- package/e2e/sidebar/sidebar.spec.ts +40 -0
- package/e2e/status/status-popover.spec.ts +94 -0
- package/e2e/terminal/terminal-init.spec.ts +28 -0
- package/e2e/terminal/terminal-reconnect.spec.ts +46 -0
- package/e2e/terminal/terminal-tabs.spec.ts +168 -0
- package/e2e/terminal/terminal.spec.ts +18 -0
- package/e2e/thinking-level.spec.ts +25 -0
- package/e2e/tsconfig.json +9 -0
- package/e2e/utils.ts +63 -0
- package/happydom.ts +75 -0
- package/index.html +23 -0
- package/package.json +77 -0
- package/playwright.config.ts +45 -0
- package/public/_headers +17 -0
- package/public/oc-theme-preload.js +35 -0
- package/script/e2e-local.ts +180 -0
- package/src/addons/serialize.test.ts +319 -0
- package/src/addons/serialize.ts +634 -0
- package/src/app.tsx +308 -0
- package/src/components/debug-bar.tsx +443 -0
- package/src/components/dialog-connect-provider.tsx +617 -0
- package/src/components/dialog-custom-provider-form.ts +158 -0
- package/src/components/dialog-custom-provider.test.ts +80 -0
- package/src/components/dialog-custom-provider.tsx +329 -0
- package/src/components/dialog-edit-project.tsx +255 -0
- package/src/components/dialog-fork.tsx +108 -0
- package/src/components/dialog-manage-models.tsx +101 -0
- package/src/components/dialog-release-notes.tsx +144 -0
- package/src/components/dialog-select-directory.tsx +392 -0
- package/src/components/dialog-select-file.tsx +466 -0
- package/src/components/dialog-select-mcp.tsx +107 -0
- package/src/components/dialog-select-model-unpaid.tsx +137 -0
- package/src/components/dialog-select-model.tsx +220 -0
- package/src/components/dialog-select-provider.tsx +86 -0
- package/src/components/dialog-select-server.tsx +649 -0
- package/src/components/dialog-settings.tsx +73 -0
- package/src/components/file-tree.test.ts +78 -0
- package/src/components/file-tree.tsx +507 -0
- package/src/components/link.tsx +26 -0
- package/src/components/model-tooltip.tsx +91 -0
- package/src/components/prompt-input/attachments.test.ts +44 -0
- package/src/components/prompt-input/attachments.ts +201 -0
- package/src/components/prompt-input/build-request-parts.test.ts +312 -0
- package/src/components/prompt-input/build-request-parts.ts +175 -0
- package/src/components/prompt-input/context-items.tsx +88 -0
- package/src/components/prompt-input/drag-overlay.tsx +25 -0
- package/src/components/prompt-input/editor-dom.test.ts +99 -0
- package/src/components/prompt-input/editor-dom.ts +148 -0
- package/src/components/prompt-input/files.ts +66 -0
- package/src/components/prompt-input/history.test.ts +153 -0
- package/src/components/prompt-input/history.ts +256 -0
- package/src/components/prompt-input/image-attachments.tsx +58 -0
- package/src/components/prompt-input/paste.ts +24 -0
- package/src/components/prompt-input/placeholder.test.ts +48 -0
- package/src/components/prompt-input/placeholder.ts +15 -0
- package/src/components/prompt-input/slash-popover.tsx +141 -0
- package/src/components/prompt-input/submit.test.ts +346 -0
- package/src/components/prompt-input/submit.ts +579 -0
- package/src/components/prompt-input.tsx +1595 -0
- package/src/components/server/server-row.tsx +130 -0
- package/src/components/session/index.ts +5 -0
- package/src/components/session/session-context-breakdown.test.ts +61 -0
- package/src/components/session/session-context-breakdown.ts +132 -0
- package/src/components/session/session-context-format.ts +20 -0
- package/src/components/session/session-context-metrics.test.ts +101 -0
- package/src/components/session/session-context-metrics.ts +82 -0
- package/src/components/session/session-context-tab.tsx +339 -0
- package/src/components/session/session-header.tsx +486 -0
- package/src/components/session/session-new-view.tsx +91 -0
- package/src/components/session/session-sortable-tab.tsx +70 -0
- package/src/components/session/session-sortable-terminal-tab.tsx +193 -0
- package/src/components/session-context-usage.tsx +122 -0
- package/src/components/settings-general.tsx +585 -0
- package/src/components/settings-keybinds.tsx +453 -0
- package/src/components/settings-list.tsx +5 -0
- package/src/components/settings-models.tsx +137 -0
- package/src/components/settings-providers.tsx +251 -0
- package/src/components/status-popover.tsx +419 -0
- package/src/components/terminal.tsx +653 -0
- package/src/components/titlebar-history.test.ts +63 -0
- package/src/components/titlebar-history.ts +57 -0
- package/src/components/titlebar.tsx +312 -0
- package/src/constants/file-picker.ts +89 -0
- package/src/context/command-keybind.test.ts +69 -0
- package/src/context/command.test.ts +25 -0
- package/src/context/command.tsx +437 -0
- package/src/context/comments.test.ts +186 -0
- package/src/context/comments.tsx +243 -0
- package/src/context/file/content-cache.ts +88 -0
- package/src/context/file/path.test.ts +360 -0
- package/src/context/file/path.ts +151 -0
- package/src/context/file/tree-store.ts +170 -0
- package/src/context/file/types.ts +41 -0
- package/src/context/file/view-cache.ts +146 -0
- package/src/context/file/watcher.test.ts +149 -0
- package/src/context/file/watcher.ts +53 -0
- package/src/context/file-content-eviction-accounting.test.ts +65 -0
- package/src/context/file.tsx +280 -0
- package/src/context/global-sdk.tsx +232 -0
- package/src/context/global-sync/bootstrap.ts +206 -0
- package/src/context/global-sync/child-store.test.ts +38 -0
- package/src/context/global-sync/child-store.ts +281 -0
- package/src/context/global-sync/event-reducer.test.ts +552 -0
- package/src/context/global-sync/event-reducer.ts +359 -0
- package/src/context/global-sync/eviction.ts +28 -0
- package/src/context/global-sync/queue.ts +83 -0
- package/src/context/global-sync/session-cache.test.ts +102 -0
- package/src/context/global-sync/session-cache.ts +62 -0
- package/src/context/global-sync/session-load.ts +25 -0
- package/src/context/global-sync/session-prefetch.test.ts +96 -0
- package/src/context/global-sync/session-prefetch.ts +100 -0
- package/src/context/global-sync/session-trim.test.ts +59 -0
- package/src/context/global-sync/session-trim.ts +56 -0
- package/src/context/global-sync/types.ts +133 -0
- package/src/context/global-sync/utils.ts +25 -0
- package/src/context/global-sync.test.ts +122 -0
- package/src/context/global-sync.tsx +408 -0
- package/src/context/highlights.tsx +233 -0
- package/src/context/language.tsx +248 -0
- package/src/context/layout-scroll.test.ts +64 -0
- package/src/context/layout-scroll.ts +126 -0
- package/src/context/layout.test.ts +69 -0
- package/src/context/layout.tsx +937 -0
- package/src/context/local.tsx +422 -0
- package/src/context/model-variant.test.ts +86 -0
- package/src/context/model-variant.ts +52 -0
- package/src/context/models.tsx +163 -0
- package/src/context/notification.tsx +373 -0
- package/src/context/permission-auto-respond.test.ts +102 -0
- package/src/context/permission-auto-respond.ts +51 -0
- package/src/context/permission.tsx +277 -0
- package/src/context/platform.tsx +99 -0
- package/src/context/prompt.tsx +297 -0
- package/src/context/sdk.tsx +49 -0
- package/src/context/server.tsx +295 -0
- package/src/context/settings.tsx +241 -0
- package/src/context/sync-optimistic.test.ts +123 -0
- package/src/context/sync.tsx +618 -0
- package/src/context/terminal-title.ts +51 -0
- package/src/context/terminal.test.ts +82 -0
- package/src/context/terminal.tsx +437 -0
- package/src/entry.tsx +144 -0
- package/src/env.d.ts +18 -0
- package/src/hooks/use-providers.ts +44 -0
- package/src/i18n/ar.ts +855 -0
- package/src/i18n/br.ts +867 -0
- package/src/i18n/bs.ts +943 -0
- package/src/i18n/da.ts +937 -0
- package/src/i18n/de.ts +879 -0
- package/src/i18n/en.ts +948 -0
- package/src/i18n/es.ts +950 -0
- package/src/i18n/fr.ts +878 -0
- package/src/i18n/ja.ts +861 -0
- package/src/i18n/ko.ts +860 -0
- package/src/i18n/no.ts +944 -0
- package/src/i18n/parity.test.ts +32 -0
- package/src/i18n/pl.ts +865 -0
- package/src/i18n/ru.ts +946 -0
- package/src/i18n/th.ts +933 -0
- package/src/i18n/tr.ts +952 -0
- package/src/i18n/zh.ts +930 -0
- package/src/i18n/zht.ts +925 -0
- package/src/index.css +29 -0
- package/src/index.ts +6 -0
- package/src/pages/directory-layout.tsx +88 -0
- package/src/pages/error.tsx +327 -0
- package/src/pages/home.tsx +131 -0
- package/src/pages/layout/deep-links.ts +50 -0
- package/src/pages/layout/helpers.test.ts +211 -0
- package/src/pages/layout/helpers.ts +98 -0
- package/src/pages/layout/inline-editor.tsx +126 -0
- package/src/pages/layout/sidebar-items.tsx +437 -0
- package/src/pages/layout/sidebar-project.tsx +384 -0
- package/src/pages/layout/sidebar-shell.tsx +125 -0
- package/src/pages/layout/sidebar-workspace.tsx +504 -0
- package/src/pages/layout.tsx +2509 -0
- package/src/pages/session/composer/index.ts +2 -0
- package/src/pages/session/composer/session-composer-region.tsx +255 -0
- package/src/pages/session/composer/session-composer-state.test.ts +128 -0
- package/src/pages/session/composer/session-composer-state.ts +249 -0
- package/src/pages/session/composer/session-followup-dock.tsx +109 -0
- package/src/pages/session/composer/session-permission-dock.tsx +74 -0
- package/src/pages/session/composer/session-question-dock.tsx +449 -0
- package/src/pages/session/composer/session-request-tree.ts +52 -0
- package/src/pages/session/composer/session-revert-dock.tsx +99 -0
- package/src/pages/session/composer/session-todo-dock.tsx +330 -0
- package/src/pages/session/file-tab-scroll.test.ts +40 -0
- package/src/pages/session/file-tab-scroll.ts +67 -0
- package/src/pages/session/file-tabs.tsx +456 -0
- package/src/pages/session/handoff.ts +36 -0
- package/src/pages/session/helpers.test.ts +181 -0
- package/src/pages/session/helpers.ts +198 -0
- package/src/pages/session/message-gesture.test.ts +62 -0
- package/src/pages/session/message-gesture.ts +21 -0
- package/src/pages/session/message-id-from-hash.ts +6 -0
- package/src/pages/session/message-timeline.tsx +1013 -0
- package/src/pages/session/review-tab.tsx +170 -0
- package/src/pages/session/session-layout.ts +20 -0
- package/src/pages/session/session-model-helpers.test.ts +51 -0
- package/src/pages/session/session-model-helpers.ts +16 -0
- package/src/pages/session/session-side-panel.tsx +453 -0
- package/src/pages/session/terminal-label.ts +16 -0
- package/src/pages/session/terminal-panel.test.ts +25 -0
- package/src/pages/session/terminal-panel.tsx +326 -0
- package/src/pages/session/use-session-commands.tsx +495 -0
- package/src/pages/session/use-session-hash-scroll.test.ts +16 -0
- package/src/pages/session/use-session-hash-scroll.ts +197 -0
- package/src/pages/session.tsx +1841 -0
- package/src/sst-env.d.ts +12 -0
- package/src/testing/model-selection.ts +80 -0
- package/src/testing/prompt.ts +56 -0
- package/src/testing/session-composer.ts +84 -0
- package/src/testing/terminal.ts +118 -0
- package/src/theme-preload.test.ts +46 -0
- package/src/utils/agent.ts +23 -0
- package/src/utils/aim.ts +138 -0
- package/src/utils/base64.ts +10 -0
- package/src/utils/comment-note.ts +88 -0
- package/src/utils/id.ts +99 -0
- package/src/utils/notification-click.test.ts +27 -0
- package/src/utils/notification-click.ts +13 -0
- package/src/utils/persist.test.ts +115 -0
- package/src/utils/persist.ts +476 -0
- package/src/utils/prompt.test.ts +44 -0
- package/src/utils/prompt.ts +203 -0
- package/src/utils/runtime-adapters.test.ts +62 -0
- package/src/utils/runtime-adapters.ts +39 -0
- package/src/utils/same.ts +6 -0
- package/src/utils/scoped-cache.test.ts +69 -0
- package/src/utils/scoped-cache.ts +104 -0
- package/src/utils/server-errors.test.ts +131 -0
- package/src/utils/server-errors.ts +80 -0
- package/src/utils/server-health.test.ts +123 -0
- package/src/utils/server-health.ts +91 -0
- package/src/utils/server.ts +22 -0
- package/src/utils/solid-dnd.tsx +49 -0
- package/src/utils/sound.ts +117 -0
- package/src/utils/terminal-writer.test.ts +64 -0
- package/src/utils/terminal-writer.ts +65 -0
- package/src/utils/time.ts +22 -0
- package/src/utils/uuid.test.ts +78 -0
- package/src/utils/uuid.ts +12 -0
- package/src/utils/worktree.test.ts +46 -0
- package/src/utils/worktree.ts +73 -0
- package/sst-env.d.ts +10 -0
- package/tsconfig.json +26 -0
- package/vite.config.ts +15 -0
- package/vite.js +26 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { Show, type Component } from "solid-js"
|
|
2
|
+
import { useLanguage } from "@/context/language"
|
|
3
|
+
|
|
4
|
+
type InputKey = "text" | "image" | "audio" | "video" | "pdf"
|
|
5
|
+
type InputMap = Record<InputKey, boolean>
|
|
6
|
+
|
|
7
|
+
type ModelInfo = {
|
|
8
|
+
id: string
|
|
9
|
+
name: string
|
|
10
|
+
provider: {
|
|
11
|
+
name: string
|
|
12
|
+
}
|
|
13
|
+
capabilities?: {
|
|
14
|
+
reasoning: boolean
|
|
15
|
+
input: InputMap
|
|
16
|
+
}
|
|
17
|
+
modalities?: {
|
|
18
|
+
input: Array<string>
|
|
19
|
+
}
|
|
20
|
+
reasoning?: boolean
|
|
21
|
+
limit: {
|
|
22
|
+
context: number
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const ModelTooltip: Component<{ model: ModelInfo; latest?: boolean; free?: boolean }> = (props) => {
|
|
27
|
+
const language = useLanguage()
|
|
28
|
+
const sourceName = (model: ModelInfo) => {
|
|
29
|
+
const value = `${model.id} ${model.name}`.toLowerCase()
|
|
30
|
+
|
|
31
|
+
if (/claude|anthropic/.test(value)) return language.t("model.provider.anthropic")
|
|
32
|
+
if (/gpt|o[1-4]|codex|openai/.test(value)) return language.t("model.provider.openai")
|
|
33
|
+
if (/gemini|palm|bard|google/.test(value)) return language.t("model.provider.google")
|
|
34
|
+
if (/grok|xai/.test(value)) return language.t("model.provider.xai")
|
|
35
|
+
if (/llama|meta/.test(value)) return language.t("model.provider.meta")
|
|
36
|
+
|
|
37
|
+
return model.provider.name
|
|
38
|
+
}
|
|
39
|
+
const inputLabel = (value: string) => {
|
|
40
|
+
if (value === "text") return language.t("model.input.text")
|
|
41
|
+
if (value === "image") return language.t("model.input.image")
|
|
42
|
+
if (value === "audio") return language.t("model.input.audio")
|
|
43
|
+
if (value === "video") return language.t("model.input.video")
|
|
44
|
+
if (value === "pdf") return language.t("model.input.pdf")
|
|
45
|
+
return value
|
|
46
|
+
}
|
|
47
|
+
const title = () => {
|
|
48
|
+
const tags: Array<string> = []
|
|
49
|
+
if (props.latest) tags.push(language.t("model.tag.latest"))
|
|
50
|
+
if (props.free) tags.push(language.t("model.tag.free"))
|
|
51
|
+
const suffix = tags.length ? ` (${tags.join(", ")})` : ""
|
|
52
|
+
return `${sourceName(props.model)} ${props.model.name}${suffix}`
|
|
53
|
+
}
|
|
54
|
+
const inputs = () => {
|
|
55
|
+
if (props.model.capabilities) {
|
|
56
|
+
const input = props.model.capabilities.input
|
|
57
|
+
const order: Array<InputKey> = ["text", "image", "audio", "video", "pdf"]
|
|
58
|
+
const entries = order.filter((key) => input[key]).map((key) => inputLabel(key))
|
|
59
|
+
return entries.length ? entries.join(", ") : undefined
|
|
60
|
+
}
|
|
61
|
+
const raw = props.model.modalities?.input
|
|
62
|
+
if (!raw) return
|
|
63
|
+
const entries = raw.map((value) => inputLabel(value))
|
|
64
|
+
return entries.length ? entries.join(", ") : undefined
|
|
65
|
+
}
|
|
66
|
+
const reasoning = () => {
|
|
67
|
+
if (props.model.capabilities)
|
|
68
|
+
return props.model.capabilities.reasoning
|
|
69
|
+
? language.t("model.tooltip.reasoning.allowed")
|
|
70
|
+
: language.t("model.tooltip.reasoning.none")
|
|
71
|
+
return props.model.reasoning
|
|
72
|
+
? language.t("model.tooltip.reasoning.allowed")
|
|
73
|
+
: language.t("model.tooltip.reasoning.none")
|
|
74
|
+
}
|
|
75
|
+
const context = () => language.t("model.tooltip.context", { limit: props.model.limit.context.toLocaleString() })
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div class="flex flex-col gap-1 py-1">
|
|
79
|
+
<div class="text-13-medium">{title()}</div>
|
|
80
|
+
<Show when={inputs()}>
|
|
81
|
+
{(value) => (
|
|
82
|
+
<div class="text-12-regular text-text-invert-base">
|
|
83
|
+
{language.t("model.tooltip.allows", { inputs: value() })}
|
|
84
|
+
</div>
|
|
85
|
+
)}
|
|
86
|
+
</Show>
|
|
87
|
+
<div class="text-12-regular text-text-invert-base">{reasoning()}</div>
|
|
88
|
+
<div class="text-12-regular text-text-invert-base">{context()}</div>
|
|
89
|
+
</div>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import { attachmentMime } from "./files"
|
|
3
|
+
import { pasteMode } from "./paste"
|
|
4
|
+
|
|
5
|
+
describe("attachmentMime", () => {
|
|
6
|
+
test("keeps PDFs when the browser reports the mime", async () => {
|
|
7
|
+
const file = new File(["%PDF-1.7"], "guide.pdf", { type: "application/pdf" })
|
|
8
|
+
expect(await attachmentMime(file)).toBe("application/pdf")
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
test("normalizes structured text types to text/plain", async () => {
|
|
12
|
+
const file = new File(['{"ok":true}\n'], "data.json", { type: "application/json" })
|
|
13
|
+
expect(await attachmentMime(file)).toBe("text/plain")
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
test("accepts text files even with a misleading browser mime", async () => {
|
|
17
|
+
const file = new File(["export const x = 1\n"], "main.ts", { type: "video/mp2t" })
|
|
18
|
+
expect(await attachmentMime(file)).toBe("text/plain")
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test("rejects binary files", async () => {
|
|
22
|
+
const file = new File([Uint8Array.of(0, 255, 1, 2)], "blob.bin", { type: "application/octet-stream" })
|
|
23
|
+
expect(await attachmentMime(file)).toBeUndefined()
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
describe("pasteMode", () => {
|
|
28
|
+
test("uses native paste for short single-line text", () => {
|
|
29
|
+
expect(pasteMode("hello world")).toBe("native")
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test("uses manual paste for multiline text", () => {
|
|
33
|
+
expect(
|
|
34
|
+
pasteMode(`{
|
|
35
|
+
"ok": true
|
|
36
|
+
}`),
|
|
37
|
+
).toBe("manual")
|
|
38
|
+
expect(pasteMode("a\r\nb")).toBe("manual")
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test("uses manual paste for large text", () => {
|
|
42
|
+
expect(pasteMode("x".repeat(8000))).toBe("manual")
|
|
43
|
+
})
|
|
44
|
+
})
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { onCleanup, onMount } from "solid-js"
|
|
2
|
+
import { showToast } from "@reign-labs/ui/toast"
|
|
3
|
+
import { usePrompt, type ContentPart, type ImageAttachmentPart } from "@/context/prompt"
|
|
4
|
+
import { useLanguage } from "@/context/language"
|
|
5
|
+
import { uuid } from "@/utils/uuid"
|
|
6
|
+
import { getCursorPosition } from "./editor-dom"
|
|
7
|
+
import { attachmentMime } from "./files"
|
|
8
|
+
import { normalizePaste, pasteMode } from "./paste"
|
|
9
|
+
|
|
10
|
+
function dataUrl(file: File, mime: string) {
|
|
11
|
+
return new Promise<string>((resolve) => {
|
|
12
|
+
const reader = new FileReader()
|
|
13
|
+
reader.addEventListener("error", () => resolve(""))
|
|
14
|
+
reader.addEventListener("load", () => {
|
|
15
|
+
const value = typeof reader.result === "string" ? reader.result : ""
|
|
16
|
+
const idx = value.indexOf(",")
|
|
17
|
+
if (idx === -1) {
|
|
18
|
+
resolve(value)
|
|
19
|
+
return
|
|
20
|
+
}
|
|
21
|
+
resolve(`data:${mime};base64,${value.slice(idx + 1)}`)
|
|
22
|
+
})
|
|
23
|
+
reader.readAsDataURL(file)
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type PromptAttachmentsInput = {
|
|
28
|
+
editor: () => HTMLDivElement | undefined
|
|
29
|
+
isDialogActive: () => boolean
|
|
30
|
+
setDraggingType: (type: "image" | "@mention" | null) => void
|
|
31
|
+
focusEditor: () => void
|
|
32
|
+
addPart: (part: ContentPart) => boolean
|
|
33
|
+
readClipboardImage?: () => Promise<File | null>
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function createPromptAttachments(input: PromptAttachmentsInput) {
|
|
37
|
+
const prompt = usePrompt()
|
|
38
|
+
const language = useLanguage()
|
|
39
|
+
|
|
40
|
+
const warn = () => {
|
|
41
|
+
showToast({
|
|
42
|
+
title: language.t("prompt.toast.pasteUnsupported.title"),
|
|
43
|
+
description: language.t("prompt.toast.pasteUnsupported.description"),
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const add = async (file: File, toast = true) => {
|
|
48
|
+
const mime = await attachmentMime(file)
|
|
49
|
+
if (!mime) {
|
|
50
|
+
if (toast) warn()
|
|
51
|
+
return false
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const editor = input.editor()
|
|
55
|
+
if (!editor) return false
|
|
56
|
+
|
|
57
|
+
const url = await dataUrl(file, mime)
|
|
58
|
+
if (!url) return false
|
|
59
|
+
|
|
60
|
+
const attachment: ImageAttachmentPart = {
|
|
61
|
+
type: "image",
|
|
62
|
+
id: uuid(),
|
|
63
|
+
filename: file.name,
|
|
64
|
+
mime,
|
|
65
|
+
dataUrl: url,
|
|
66
|
+
}
|
|
67
|
+
const cursor = prompt.cursor() ?? getCursorPosition(editor)
|
|
68
|
+
prompt.set([...prompt.current(), attachment], cursor)
|
|
69
|
+
return true
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const addAttachment = (file: File) => add(file)
|
|
73
|
+
|
|
74
|
+
const addAttachments = async (files: File[], toast = true) => {
|
|
75
|
+
let found = false
|
|
76
|
+
|
|
77
|
+
for (const file of files) {
|
|
78
|
+
const ok = await add(file, false)
|
|
79
|
+
if (ok) found = true
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!found && files.length > 0 && toast) warn()
|
|
83
|
+
return found
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const removeAttachment = (id: string) => {
|
|
87
|
+
const current = prompt.current()
|
|
88
|
+
const next = current.filter((part) => part.type !== "image" || part.id !== id)
|
|
89
|
+
prompt.set(next, prompt.cursor())
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const handlePaste = async (event: ClipboardEvent) => {
|
|
93
|
+
const clipboardData = event.clipboardData
|
|
94
|
+
if (!clipboardData) return
|
|
95
|
+
|
|
96
|
+
event.preventDefault()
|
|
97
|
+
event.stopPropagation()
|
|
98
|
+
|
|
99
|
+
const files = Array.from(clipboardData.items).flatMap((item) => {
|
|
100
|
+
if (item.kind !== "file") return []
|
|
101
|
+
const file = item.getAsFile()
|
|
102
|
+
return file ? [file] : []
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
if (files.length > 0) {
|
|
106
|
+
await addAttachments(files)
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const plainText = clipboardData.getData("text/plain") ?? ""
|
|
111
|
+
|
|
112
|
+
// Desktop: Browser clipboard has no images and no text, try platform's native clipboard for images
|
|
113
|
+
if (input.readClipboardImage && !plainText) {
|
|
114
|
+
const file = await input.readClipboardImage()
|
|
115
|
+
if (file) {
|
|
116
|
+
await addAttachment(file)
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!plainText) return
|
|
122
|
+
|
|
123
|
+
const text = normalizePaste(plainText)
|
|
124
|
+
|
|
125
|
+
const put = () => {
|
|
126
|
+
if (input.addPart({ type: "text", content: text, start: 0, end: 0 })) return true
|
|
127
|
+
input.focusEditor()
|
|
128
|
+
return input.addPart({ type: "text", content: text, start: 0, end: 0 })
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (pasteMode(text) === "manual") {
|
|
132
|
+
put()
|
|
133
|
+
return
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const inserted = typeof document.execCommand === "function" && document.execCommand("insertText", false, text)
|
|
137
|
+
if (inserted) return
|
|
138
|
+
|
|
139
|
+
put()
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const handleGlobalDragOver = (event: DragEvent) => {
|
|
143
|
+
if (input.isDialogActive()) return
|
|
144
|
+
|
|
145
|
+
event.preventDefault()
|
|
146
|
+
const hasFiles = event.dataTransfer?.types.includes("Files")
|
|
147
|
+
const hasText = event.dataTransfer?.types.includes("text/plain")
|
|
148
|
+
if (hasFiles) {
|
|
149
|
+
input.setDraggingType("image")
|
|
150
|
+
} else if (hasText) {
|
|
151
|
+
input.setDraggingType("@mention")
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const handleGlobalDragLeave = (event: DragEvent) => {
|
|
156
|
+
if (input.isDialogActive()) return
|
|
157
|
+
if (!event.relatedTarget) {
|
|
158
|
+
input.setDraggingType(null)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const handleGlobalDrop = async (event: DragEvent) => {
|
|
163
|
+
if (input.isDialogActive()) return
|
|
164
|
+
|
|
165
|
+
event.preventDefault()
|
|
166
|
+
input.setDraggingType(null)
|
|
167
|
+
|
|
168
|
+
const plainText = event.dataTransfer?.getData("text/plain")
|
|
169
|
+
const filePrefix = "file:"
|
|
170
|
+
if (plainText?.startsWith(filePrefix)) {
|
|
171
|
+
const filePath = plainText.slice(filePrefix.length)
|
|
172
|
+
input.focusEditor()
|
|
173
|
+
input.addPart({ type: "file", path: filePath, content: "@" + filePath, start: 0, end: 0 })
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const dropped = event.dataTransfer?.files
|
|
178
|
+
if (!dropped) return
|
|
179
|
+
|
|
180
|
+
await addAttachments(Array.from(dropped))
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
onMount(() => {
|
|
184
|
+
document.addEventListener("dragover", handleGlobalDragOver)
|
|
185
|
+
document.addEventListener("dragleave", handleGlobalDragLeave)
|
|
186
|
+
document.addEventListener("drop", handleGlobalDrop)
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
onCleanup(() => {
|
|
190
|
+
document.removeEventListener("dragover", handleGlobalDragOver)
|
|
191
|
+
document.removeEventListener("dragleave", handleGlobalDragLeave)
|
|
192
|
+
document.removeEventListener("drop", handleGlobalDrop)
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
addAttachment,
|
|
197
|
+
addAttachments,
|
|
198
|
+
removeAttachment,
|
|
199
|
+
handlePaste,
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import type { Prompt } from "@/context/prompt"
|
|
3
|
+
import { buildRequestParts } from "./build-request-parts"
|
|
4
|
+
|
|
5
|
+
describe("buildRequestParts", () => {
|
|
6
|
+
test("builds typed request and optimistic parts without cast path", () => {
|
|
7
|
+
const prompt: Prompt = [
|
|
8
|
+
{ type: "text", content: "hello", start: 0, end: 5 },
|
|
9
|
+
{
|
|
10
|
+
type: "file",
|
|
11
|
+
path: "src/foo.ts",
|
|
12
|
+
content: "@src/foo.ts",
|
|
13
|
+
start: 5,
|
|
14
|
+
end: 16,
|
|
15
|
+
selection: { startLine: 4, startChar: 1, endLine: 6, endChar: 1 },
|
|
16
|
+
},
|
|
17
|
+
{ type: "agent", name: "planner", content: "@planner", start: 16, end: 24 },
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
const result = buildRequestParts({
|
|
21
|
+
prompt,
|
|
22
|
+
context: [{ key: "ctx:1", type: "file", path: "src/bar.ts", comment: "check this" }],
|
|
23
|
+
images: [
|
|
24
|
+
{ type: "image", id: "img_1", filename: "a.png", mime: "image/png", dataUrl: "data:image/png;base64,AAA" },
|
|
25
|
+
],
|
|
26
|
+
text: "hello @src/foo.ts @planner",
|
|
27
|
+
messageID: "msg_1",
|
|
28
|
+
sessionID: "ses_1",
|
|
29
|
+
sessionDirectory: "/repo",
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
expect(result.requestParts[0]?.type).toBe("text")
|
|
33
|
+
expect(result.requestParts.some((part) => part.type === "agent")).toBe(true)
|
|
34
|
+
expect(
|
|
35
|
+
result.requestParts.some((part) => part.type === "file" && part.url.startsWith("file:///repo/src/foo.ts")),
|
|
36
|
+
).toBe(true)
|
|
37
|
+
expect(result.requestParts.some((part) => part.type === "text" && part.synthetic)).toBe(true)
|
|
38
|
+
expect(
|
|
39
|
+
result.requestParts.some(
|
|
40
|
+
(part) =>
|
|
41
|
+
part.type === "text" &&
|
|
42
|
+
part.synthetic &&
|
|
43
|
+
part.metadata?.opencodeComment &&
|
|
44
|
+
(part.metadata.opencodeComment as { comment?: string }).comment === "check this",
|
|
45
|
+
),
|
|
46
|
+
).toBe(true)
|
|
47
|
+
|
|
48
|
+
expect(result.optimisticParts).toHaveLength(result.requestParts.length)
|
|
49
|
+
expect(result.optimisticParts.every((part) => part.sessionID === "ses_1" && part.messageID === "msg_1")).toBe(true)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test("keeps multiple uploaded attachments in order", () => {
|
|
53
|
+
const result = buildRequestParts({
|
|
54
|
+
prompt: [{ type: "text", content: "check these", start: 0, end: 11 }],
|
|
55
|
+
context: [],
|
|
56
|
+
images: [
|
|
57
|
+
{ type: "image", id: "img_1", filename: "a.png", mime: "image/png", dataUrl: "data:image/png;base64,AAA" },
|
|
58
|
+
{
|
|
59
|
+
type: "image",
|
|
60
|
+
id: "img_2",
|
|
61
|
+
filename: "b.pdf",
|
|
62
|
+
mime: "application/pdf",
|
|
63
|
+
dataUrl: "data:application/pdf;base64,BBB",
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
text: "check these",
|
|
67
|
+
messageID: "msg_multi",
|
|
68
|
+
sessionID: "ses_multi",
|
|
69
|
+
sessionDirectory: "/repo",
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
const files = result.requestParts.filter((part) => part.type === "file" && part.url.startsWith("data:"))
|
|
73
|
+
|
|
74
|
+
expect(files).toHaveLength(2)
|
|
75
|
+
expect(files.map((part) => (part.type === "file" ? part.filename : ""))).toEqual(["a.png", "b.pdf"])
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test("deduplicates context files when prompt already includes same path", () => {
|
|
79
|
+
const prompt: Prompt = [{ type: "file", path: "src/foo.ts", content: "@src/foo.ts", start: 0, end: 11 }]
|
|
80
|
+
|
|
81
|
+
const result = buildRequestParts({
|
|
82
|
+
prompt,
|
|
83
|
+
context: [
|
|
84
|
+
{ key: "ctx:dup", type: "file", path: "src/foo.ts" },
|
|
85
|
+
{ key: "ctx:comment", type: "file", path: "src/foo.ts", comment: "focus here" },
|
|
86
|
+
],
|
|
87
|
+
images: [],
|
|
88
|
+
text: "@src/foo.ts",
|
|
89
|
+
messageID: "msg_2",
|
|
90
|
+
sessionID: "ses_2",
|
|
91
|
+
sessionDirectory: "/repo",
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
const fooFiles = result.requestParts.filter(
|
|
95
|
+
(part) => part.type === "file" && part.url.startsWith("file:///repo/src/foo.ts"),
|
|
96
|
+
)
|
|
97
|
+
const synthetic = result.requestParts.filter((part) => part.type === "text" && part.synthetic)
|
|
98
|
+
|
|
99
|
+
expect(fooFiles).toHaveLength(2)
|
|
100
|
+
expect(synthetic).toHaveLength(1)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
test("handles Windows paths correctly (simulated on macOS)", () => {
|
|
104
|
+
const prompt: Prompt = [{ type: "file", path: "src\\foo.ts", content: "@src\\foo.ts", start: 0, end: 11 }]
|
|
105
|
+
|
|
106
|
+
const result = buildRequestParts({
|
|
107
|
+
prompt,
|
|
108
|
+
context: [],
|
|
109
|
+
images: [],
|
|
110
|
+
text: "@src\\foo.ts",
|
|
111
|
+
messageID: "msg_win_1",
|
|
112
|
+
sessionID: "ses_win_1",
|
|
113
|
+
sessionDirectory: "D:\\projects\\myapp", // Windows path
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
// Should create valid file URLs
|
|
117
|
+
const filePart = result.requestParts.find((part) => part.type === "file")
|
|
118
|
+
expect(filePart).toBeDefined()
|
|
119
|
+
if (filePart?.type === "file") {
|
|
120
|
+
// URL should be parseable
|
|
121
|
+
expect(() => new URL(filePart.url)).not.toThrow()
|
|
122
|
+
// Should not have encoded backslashes in wrong place
|
|
123
|
+
expect(filePart.url).not.toContain("%5C")
|
|
124
|
+
// Should have normalized to forward slashes
|
|
125
|
+
expect(filePart.url).toContain("/src/foo.ts")
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
test("handles Windows absolute path with special characters", () => {
|
|
130
|
+
const prompt: Prompt = [{ type: "file", path: "file#name.txt", content: "@file#name.txt", start: 0, end: 14 }]
|
|
131
|
+
|
|
132
|
+
const result = buildRequestParts({
|
|
133
|
+
prompt,
|
|
134
|
+
context: [],
|
|
135
|
+
images: [],
|
|
136
|
+
text: "@file#name.txt",
|
|
137
|
+
messageID: "msg_win_2",
|
|
138
|
+
sessionID: "ses_win_2",
|
|
139
|
+
sessionDirectory: "C:\\Users\\test\\Documents", // Windows path
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
const filePart = result.requestParts.find((part) => part.type === "file")
|
|
143
|
+
expect(filePart).toBeDefined()
|
|
144
|
+
if (filePart?.type === "file") {
|
|
145
|
+
// URL should be parseable
|
|
146
|
+
expect(() => new URL(filePart.url)).not.toThrow()
|
|
147
|
+
// Special chars should be encoded
|
|
148
|
+
expect(filePart.url).toContain("file%23name.txt")
|
|
149
|
+
// Should have Windows drive letter properly encoded
|
|
150
|
+
expect(filePart.url).toMatch(/file:\/\/\/[A-Z]:/)
|
|
151
|
+
}
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
test("handles Linux absolute paths correctly", () => {
|
|
155
|
+
const prompt: Prompt = [{ type: "file", path: "src/app.ts", content: "@src/app.ts", start: 0, end: 10 }]
|
|
156
|
+
|
|
157
|
+
const result = buildRequestParts({
|
|
158
|
+
prompt,
|
|
159
|
+
context: [],
|
|
160
|
+
images: [],
|
|
161
|
+
text: "@src/app.ts",
|
|
162
|
+
messageID: "msg_linux_1",
|
|
163
|
+
sessionID: "ses_linux_1",
|
|
164
|
+
sessionDirectory: "/home/user/project",
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
const filePart = result.requestParts.find((part) => part.type === "file")
|
|
168
|
+
expect(filePart).toBeDefined()
|
|
169
|
+
if (filePart?.type === "file") {
|
|
170
|
+
// URL should be parseable
|
|
171
|
+
expect(() => new URL(filePart.url)).not.toThrow()
|
|
172
|
+
// Should be a normal Unix path
|
|
173
|
+
expect(filePart.url).toBe("file:///home/user/project/src/app.ts")
|
|
174
|
+
}
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
test("handles macOS paths correctly", () => {
|
|
178
|
+
const prompt: Prompt = [{ type: "file", path: "README.md", content: "@README.md", start: 0, end: 9 }]
|
|
179
|
+
|
|
180
|
+
const result = buildRequestParts({
|
|
181
|
+
prompt,
|
|
182
|
+
context: [],
|
|
183
|
+
images: [],
|
|
184
|
+
text: "@README.md",
|
|
185
|
+
messageID: "msg_mac_1",
|
|
186
|
+
sessionID: "ses_mac_1",
|
|
187
|
+
sessionDirectory: "/Users/kelvin/Projects/opencode",
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
const filePart = result.requestParts.find((part) => part.type === "file")
|
|
191
|
+
expect(filePart).toBeDefined()
|
|
192
|
+
if (filePart?.type === "file") {
|
|
193
|
+
// URL should be parseable
|
|
194
|
+
expect(() => new URL(filePart.url)).not.toThrow()
|
|
195
|
+
// Should be a normal Unix path
|
|
196
|
+
expect(filePart.url).toBe("file:///Users/kelvin/Projects/opencode/README.md")
|
|
197
|
+
}
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
test("handles context files with Windows paths", () => {
|
|
201
|
+
const prompt: Prompt = []
|
|
202
|
+
|
|
203
|
+
const result = buildRequestParts({
|
|
204
|
+
prompt,
|
|
205
|
+
context: [
|
|
206
|
+
{ key: "ctx:1", type: "file", path: "src\\utils\\helper.ts" },
|
|
207
|
+
{ key: "ctx:2", type: "file", path: "test\\unit.test.ts", comment: "check tests" },
|
|
208
|
+
],
|
|
209
|
+
images: [],
|
|
210
|
+
text: "test",
|
|
211
|
+
messageID: "msg_win_ctx",
|
|
212
|
+
sessionID: "ses_win_ctx",
|
|
213
|
+
sessionDirectory: "D:\\workspace\\app",
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
const fileParts = result.requestParts.filter((part) => part.type === "file")
|
|
217
|
+
expect(fileParts).toHaveLength(2)
|
|
218
|
+
|
|
219
|
+
// All file URLs should be valid
|
|
220
|
+
fileParts.forEach((part) => {
|
|
221
|
+
if (part.type === "file") {
|
|
222
|
+
expect(() => new URL(part.url)).not.toThrow()
|
|
223
|
+
expect(part.url).not.toContain("%5C") // No encoded backslashes
|
|
224
|
+
}
|
|
225
|
+
})
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
test("handles absolute Windows paths (user manually specifies full path)", () => {
|
|
229
|
+
const prompt: Prompt = [
|
|
230
|
+
{ type: "file", path: "D:\\other\\project\\file.ts", content: "@D:\\other\\project\\file.ts", start: 0, end: 25 },
|
|
231
|
+
]
|
|
232
|
+
|
|
233
|
+
const result = buildRequestParts({
|
|
234
|
+
prompt,
|
|
235
|
+
context: [],
|
|
236
|
+
images: [],
|
|
237
|
+
text: "@D:\\other\\project\\file.ts",
|
|
238
|
+
messageID: "msg_abs",
|
|
239
|
+
sessionID: "ses_abs",
|
|
240
|
+
sessionDirectory: "C:\\current\\project",
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
const filePart = result.requestParts.find((part) => part.type === "file")
|
|
244
|
+
expect(filePart).toBeDefined()
|
|
245
|
+
if (filePart?.type === "file") {
|
|
246
|
+
// Should handle absolute path that differs from sessionDirectory
|
|
247
|
+
expect(() => new URL(filePart.url)).not.toThrow()
|
|
248
|
+
expect(filePart.url).toContain("/D:/other/project/file.ts")
|
|
249
|
+
}
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
test("handles selection with query parameters on Windows", () => {
|
|
253
|
+
const prompt: Prompt = [
|
|
254
|
+
{
|
|
255
|
+
type: "file",
|
|
256
|
+
path: "src\\App.tsx",
|
|
257
|
+
content: "@src\\App.tsx",
|
|
258
|
+
start: 0,
|
|
259
|
+
end: 11,
|
|
260
|
+
selection: { startLine: 10, startChar: 0, endLine: 20, endChar: 5 },
|
|
261
|
+
},
|
|
262
|
+
]
|
|
263
|
+
|
|
264
|
+
const result = buildRequestParts({
|
|
265
|
+
prompt,
|
|
266
|
+
context: [],
|
|
267
|
+
images: [],
|
|
268
|
+
text: "@src\\App.tsx",
|
|
269
|
+
messageID: "msg_sel",
|
|
270
|
+
sessionID: "ses_sel",
|
|
271
|
+
sessionDirectory: "C:\\project",
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
const filePart = result.requestParts.find((part) => part.type === "file")
|
|
275
|
+
expect(filePart).toBeDefined()
|
|
276
|
+
if (filePart?.type === "file") {
|
|
277
|
+
// Should have query parameters
|
|
278
|
+
expect(filePart.url).toContain("?start=10&end=20")
|
|
279
|
+
// Should be valid URL
|
|
280
|
+
expect(() => new URL(filePart.url)).not.toThrow()
|
|
281
|
+
// Query params should parse correctly
|
|
282
|
+
const url = new URL(filePart.url)
|
|
283
|
+
expect(url.searchParams.get("start")).toBe("10")
|
|
284
|
+
expect(url.searchParams.get("end")).toBe("20")
|
|
285
|
+
}
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
test("handles file paths with dots and special segments on Windows", () => {
|
|
289
|
+
const prompt: Prompt = [
|
|
290
|
+
{ type: "file", path: "..\\..\\shared\\util.ts", content: "@..\\..\\shared\\util.ts", start: 0, end: 21 },
|
|
291
|
+
]
|
|
292
|
+
|
|
293
|
+
const result = buildRequestParts({
|
|
294
|
+
prompt,
|
|
295
|
+
context: [],
|
|
296
|
+
images: [],
|
|
297
|
+
text: "@..\\..\\shared\\util.ts",
|
|
298
|
+
messageID: "msg_dots",
|
|
299
|
+
sessionID: "ses_dots",
|
|
300
|
+
sessionDirectory: "C:\\projects\\myapp\\src",
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
const filePart = result.requestParts.find((part) => part.type === "file")
|
|
304
|
+
expect(filePart).toBeDefined()
|
|
305
|
+
if (filePart?.type === "file") {
|
|
306
|
+
// Should be valid URL
|
|
307
|
+
expect(() => new URL(filePart.url)).not.toThrow()
|
|
308
|
+
// Should preserve .. segments (backend normalizes)
|
|
309
|
+
expect(filePart.url).toContain("/..")
|
|
310
|
+
}
|
|
311
|
+
})
|
|
312
|
+
})
|