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,243 @@
|
|
|
1
|
+
import { batch, createMemo, createRoot, onCleanup } from "solid-js"
|
|
2
|
+
import { createStore, reconcile, type SetStoreFunction, type Store } from "solid-js/store"
|
|
3
|
+
import { createSimpleContext } from "@reign-labs/ui/context"
|
|
4
|
+
import { useParams } from "@solidjs/router"
|
|
5
|
+
import { Persist, persisted } from "@/utils/persist"
|
|
6
|
+
import { createScopedCache } from "@/utils/scoped-cache"
|
|
7
|
+
import { uuid } from "@/utils/uuid"
|
|
8
|
+
import type { SelectedLineRange } from "@/context/file"
|
|
9
|
+
|
|
10
|
+
export type LineComment = {
|
|
11
|
+
id: string
|
|
12
|
+
file: string
|
|
13
|
+
selection: SelectedLineRange
|
|
14
|
+
comment: string
|
|
15
|
+
time: number
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type CommentFocus = { file: string; id: string }
|
|
19
|
+
|
|
20
|
+
const WORKSPACE_KEY = "__workspace__"
|
|
21
|
+
const MAX_COMMENT_SESSIONS = 20
|
|
22
|
+
|
|
23
|
+
function sessionKey(dir: string, id: string | undefined) {
|
|
24
|
+
return `${dir}\n${id ?? WORKSPACE_KEY}`
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function decodeSessionKey(key: string) {
|
|
28
|
+
const split = key.lastIndexOf("\n")
|
|
29
|
+
if (split < 0) return { dir: key, id: WORKSPACE_KEY }
|
|
30
|
+
return {
|
|
31
|
+
dir: key.slice(0, split),
|
|
32
|
+
id: key.slice(split + 1),
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
type CommentStore = {
|
|
37
|
+
comments: Record<string, LineComment[]>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function aggregate(comments: Record<string, LineComment[]>) {
|
|
41
|
+
return Object.keys(comments)
|
|
42
|
+
.flatMap((file) => comments[file] ?? [])
|
|
43
|
+
.slice()
|
|
44
|
+
.sort((a, b) => a.time - b.time)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function cloneSelection(selection: SelectedLineRange): SelectedLineRange {
|
|
48
|
+
const next: SelectedLineRange = {
|
|
49
|
+
start: selection.start,
|
|
50
|
+
end: selection.end,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (selection.side) next.side = selection.side
|
|
54
|
+
if (selection.endSide) next.endSide = selection.endSide
|
|
55
|
+
return next
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function cloneComment(comment: LineComment): LineComment {
|
|
59
|
+
return {
|
|
60
|
+
...comment,
|
|
61
|
+
selection: cloneSelection(comment.selection),
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function group(comments: LineComment[]) {
|
|
66
|
+
return comments.reduce<Record<string, LineComment[]>>((acc, comment) => {
|
|
67
|
+
const list = acc[comment.file]
|
|
68
|
+
const next = cloneComment(comment)
|
|
69
|
+
if (list) {
|
|
70
|
+
list.push(next)
|
|
71
|
+
return acc
|
|
72
|
+
}
|
|
73
|
+
acc[comment.file] = [next]
|
|
74
|
+
return acc
|
|
75
|
+
}, {})
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function createCommentSessionState(store: Store<CommentStore>, setStore: SetStoreFunction<CommentStore>) {
|
|
79
|
+
const [state, setState] = createStore({
|
|
80
|
+
focus: null as CommentFocus | null,
|
|
81
|
+
active: null as CommentFocus | null,
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
const all = () => aggregate(store.comments)
|
|
85
|
+
|
|
86
|
+
const setRef = (
|
|
87
|
+
key: "focus" | "active",
|
|
88
|
+
value: CommentFocus | null | ((value: CommentFocus | null) => CommentFocus | null),
|
|
89
|
+
) => setState(key, value)
|
|
90
|
+
|
|
91
|
+
const setFocus = (value: CommentFocus | null | ((value: CommentFocus | null) => CommentFocus | null)) =>
|
|
92
|
+
setRef("focus", value)
|
|
93
|
+
|
|
94
|
+
const setActive = (value: CommentFocus | null | ((value: CommentFocus | null) => CommentFocus | null)) =>
|
|
95
|
+
setRef("active", value)
|
|
96
|
+
|
|
97
|
+
const list = (file: string) => store.comments[file] ?? []
|
|
98
|
+
|
|
99
|
+
const add = (input: Omit<LineComment, "id" | "time">) => {
|
|
100
|
+
const next: LineComment = {
|
|
101
|
+
id: uuid(),
|
|
102
|
+
time: Date.now(),
|
|
103
|
+
...input,
|
|
104
|
+
selection: cloneSelection(input.selection),
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
batch(() => {
|
|
108
|
+
setStore("comments", input.file, (items) => [...(items ?? []), next])
|
|
109
|
+
setFocus({ file: input.file, id: next.id })
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
return next
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const remove = (file: string, id: string) => {
|
|
116
|
+
batch(() => {
|
|
117
|
+
setStore("comments", file, (items) => (items ?? []).filter((item) => item.id !== id))
|
|
118
|
+
setFocus((current) => (current?.file === file && current.id === id ? null : current))
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const update = (file: string, id: string, comment: string) => {
|
|
123
|
+
setStore("comments", file, (items) =>
|
|
124
|
+
(items ?? []).map((item) => {
|
|
125
|
+
if (item.id !== id) return item
|
|
126
|
+
return { ...item, comment }
|
|
127
|
+
}),
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const replace = (comments: LineComment[]) => {
|
|
132
|
+
batch(() => {
|
|
133
|
+
setStore("comments", reconcile(group(comments)))
|
|
134
|
+
setFocus(null)
|
|
135
|
+
setActive(null)
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const clear = () => {
|
|
140
|
+
batch(() => {
|
|
141
|
+
setStore("comments", reconcile({}))
|
|
142
|
+
setFocus(null)
|
|
143
|
+
setActive(null)
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
list,
|
|
149
|
+
all,
|
|
150
|
+
add,
|
|
151
|
+
remove,
|
|
152
|
+
update,
|
|
153
|
+
replace,
|
|
154
|
+
clear,
|
|
155
|
+
focus: () => state.focus,
|
|
156
|
+
setFocus,
|
|
157
|
+
clearFocus: () => setRef("focus", null),
|
|
158
|
+
active: () => state.active,
|
|
159
|
+
setActive,
|
|
160
|
+
clearActive: () => setRef("active", null),
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function createCommentSessionForTest(comments: Record<string, LineComment[]> = {}) {
|
|
165
|
+
const [store, setStore] = createStore<CommentStore>({ comments })
|
|
166
|
+
return createCommentSessionState(store, setStore)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function createCommentSession(dir: string, id: string | undefined) {
|
|
170
|
+
const legacy = `${dir}/comments${id ? "/" + id : ""}.v1`
|
|
171
|
+
|
|
172
|
+
const [store, setStore, _, ready] = persisted(
|
|
173
|
+
Persist.scoped(dir, id, "comments", [legacy]),
|
|
174
|
+
createStore<CommentStore>({
|
|
175
|
+
comments: {},
|
|
176
|
+
}),
|
|
177
|
+
)
|
|
178
|
+
const session = createCommentSessionState(store, setStore)
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
ready,
|
|
182
|
+
list: session.list,
|
|
183
|
+
all: session.all,
|
|
184
|
+
add: session.add,
|
|
185
|
+
remove: session.remove,
|
|
186
|
+
update: session.update,
|
|
187
|
+
replace: session.replace,
|
|
188
|
+
clear: session.clear,
|
|
189
|
+
focus: session.focus,
|
|
190
|
+
setFocus: session.setFocus,
|
|
191
|
+
clearFocus: session.clearFocus,
|
|
192
|
+
active: session.active,
|
|
193
|
+
setActive: session.setActive,
|
|
194
|
+
clearActive: session.clearActive,
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export const { use: useComments, provider: CommentsProvider } = createSimpleContext({
|
|
199
|
+
name: "Comments",
|
|
200
|
+
gate: false,
|
|
201
|
+
init: () => {
|
|
202
|
+
const params = useParams()
|
|
203
|
+
const cache = createScopedCache(
|
|
204
|
+
(key) => {
|
|
205
|
+
const decoded = decodeSessionKey(key)
|
|
206
|
+
return createRoot((dispose) => ({
|
|
207
|
+
value: createCommentSession(decoded.dir, decoded.id === WORKSPACE_KEY ? undefined : decoded.id),
|
|
208
|
+
dispose,
|
|
209
|
+
}))
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
maxEntries: MAX_COMMENT_SESSIONS,
|
|
213
|
+
dispose: (entry) => entry.dispose(),
|
|
214
|
+
},
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
onCleanup(() => cache.clear())
|
|
218
|
+
|
|
219
|
+
const load = (dir: string, id: string | undefined) => {
|
|
220
|
+
const key = sessionKey(dir, id)
|
|
221
|
+
return cache.get(key).value
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const session = createMemo(() => load(params.dir!, params.id))
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
ready: () => session().ready(),
|
|
228
|
+
list: (file: string) => session().list(file),
|
|
229
|
+
all: () => session().all(),
|
|
230
|
+
add: (input: Omit<LineComment, "id" | "time">) => session().add(input),
|
|
231
|
+
remove: (file: string, id: string) => session().remove(file, id),
|
|
232
|
+
update: (file: string, id: string, comment: string) => session().update(file, id, comment),
|
|
233
|
+
replace: (comments: LineComment[]) => session().replace(comments),
|
|
234
|
+
clear: () => session().clear(),
|
|
235
|
+
focus: () => session().focus(),
|
|
236
|
+
setFocus: (focus: CommentFocus | null) => session().setFocus(focus),
|
|
237
|
+
clearFocus: () => session().clearFocus(),
|
|
238
|
+
active: () => session().active(),
|
|
239
|
+
setActive: (active: CommentFocus | null) => session().setActive(active),
|
|
240
|
+
clearActive: () => session().clearActive(),
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
})
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { FileContent } from "@reign-labs/sdk/v2"
|
|
2
|
+
|
|
3
|
+
const MAX_FILE_CONTENT_ENTRIES = 40
|
|
4
|
+
const MAX_FILE_CONTENT_BYTES = 20 * 1024 * 1024
|
|
5
|
+
|
|
6
|
+
const lru = new Map<string, number>()
|
|
7
|
+
let total = 0
|
|
8
|
+
|
|
9
|
+
export function approxBytes(content: FileContent) {
|
|
10
|
+
const patchBytes =
|
|
11
|
+
content.patch?.hunks.reduce((sum, hunk) => {
|
|
12
|
+
return sum + hunk.lines.reduce((lineSum, line) => lineSum + line.length, 0)
|
|
13
|
+
}, 0) ?? 0
|
|
14
|
+
|
|
15
|
+
return (content.content.length + (content.diff?.length ?? 0) + patchBytes) * 2
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function setBytes(path: string, nextBytes: number) {
|
|
19
|
+
const prev = lru.get(path)
|
|
20
|
+
if (prev !== undefined) total -= prev
|
|
21
|
+
lru.delete(path)
|
|
22
|
+
lru.set(path, nextBytes)
|
|
23
|
+
total += nextBytes
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function touch(path: string, bytes?: number) {
|
|
27
|
+
const prev = lru.get(path)
|
|
28
|
+
if (prev === undefined && bytes === undefined) return
|
|
29
|
+
setBytes(path, bytes ?? prev ?? 0)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function remove(path: string) {
|
|
33
|
+
const prev = lru.get(path)
|
|
34
|
+
if (prev === undefined) return
|
|
35
|
+
lru.delete(path)
|
|
36
|
+
total -= prev
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function reset() {
|
|
40
|
+
lru.clear()
|
|
41
|
+
total = 0
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function evictContentLru(keep: Set<string> | undefined, evict: (path: string) => void) {
|
|
45
|
+
const set = keep ?? new Set<string>()
|
|
46
|
+
|
|
47
|
+
while (lru.size > MAX_FILE_CONTENT_ENTRIES || total > MAX_FILE_CONTENT_BYTES) {
|
|
48
|
+
const path = lru.keys().next().value
|
|
49
|
+
if (!path) return
|
|
50
|
+
|
|
51
|
+
if (set.has(path)) {
|
|
52
|
+
touch(path)
|
|
53
|
+
if (lru.size <= set.size) return
|
|
54
|
+
continue
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
remove(path)
|
|
58
|
+
evict(path)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function resetFileContentLru() {
|
|
63
|
+
reset()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function setFileContentBytes(path: string, bytes: number) {
|
|
67
|
+
setBytes(path, bytes)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function removeFileContentBytes(path: string) {
|
|
71
|
+
remove(path)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function touchFileContent(path: string, bytes?: number) {
|
|
75
|
+
touch(path, bytes)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function getFileContentBytesTotal() {
|
|
79
|
+
return total
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function getFileContentEntryCount() {
|
|
83
|
+
return lru.size
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function hasFileContent(path: string) {
|
|
87
|
+
return lru.has(path)
|
|
88
|
+
}
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import { createPathHelpers, stripQueryAndHash, unquoteGitPath, encodeFilePath } from "./path"
|
|
3
|
+
|
|
4
|
+
describe("file path helpers", () => {
|
|
5
|
+
test("normalizes file inputs against workspace root", () => {
|
|
6
|
+
const path = createPathHelpers(() => "/repo")
|
|
7
|
+
expect(path.normalize("file:///repo/src/app.ts?x=1#h")).toBe("src/app.ts")
|
|
8
|
+
expect(path.normalize("/repo/src/app.ts")).toBe("src/app.ts")
|
|
9
|
+
expect(path.normalize("./src/app.ts")).toBe("src/app.ts")
|
|
10
|
+
expect(path.normalizeDir("src/components///")).toBe("src/components")
|
|
11
|
+
expect(path.tab("src/app.ts")).toBe("file://src/app.ts")
|
|
12
|
+
expect(path.pathFromTab("file://src/app.ts")).toBe("src/app.ts")
|
|
13
|
+
expect(path.pathFromTab("other://src/app.ts")).toBeUndefined()
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
test("normalizes Windows absolute paths with mixed separators", () => {
|
|
17
|
+
const path = createPathHelpers(() => "C:\\repo")
|
|
18
|
+
expect(path.normalize("C:\\repo\\src\\app.ts")).toBe("src\\app.ts")
|
|
19
|
+
expect(path.normalize("C:/repo/src/app.ts")).toBe("src/app.ts")
|
|
20
|
+
expect(path.normalize("file://C:/repo/src/app.ts")).toBe("src/app.ts")
|
|
21
|
+
expect(path.normalize("c:\\repo\\src\\app.ts")).toBe("src\\app.ts")
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test("keeps query/hash stripping behavior stable", () => {
|
|
25
|
+
expect(stripQueryAndHash("a/b.ts#L12?x=1")).toBe("a/b.ts")
|
|
26
|
+
expect(stripQueryAndHash("a/b.ts?x=1#L12")).toBe("a/b.ts")
|
|
27
|
+
expect(stripQueryAndHash("a/b.ts")).toBe("a/b.ts")
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test("unquotes git escaped octal path strings", () => {
|
|
31
|
+
expect(unquoteGitPath('"a/\\303\\251.txt"')).toBe("a/\u00e9.txt")
|
|
32
|
+
expect(unquoteGitPath('"plain\\nname"')).toBe("plain\nname")
|
|
33
|
+
expect(unquoteGitPath("a/b/c.ts")).toBe("a/b/c.ts")
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
describe("encodeFilePath", () => {
|
|
38
|
+
describe("Linux/Unix paths", () => {
|
|
39
|
+
test("should handle Linux absolute path", () => {
|
|
40
|
+
const linuxPath = "/home/user/project/README.md"
|
|
41
|
+
const result = encodeFilePath(linuxPath)
|
|
42
|
+
const fileUrl = `file://${result}`
|
|
43
|
+
|
|
44
|
+
// Should create a valid URL
|
|
45
|
+
expect(() => new URL(fileUrl)).not.toThrow()
|
|
46
|
+
expect(result).toBe("/home/user/project/README.md")
|
|
47
|
+
|
|
48
|
+
const url = new URL(fileUrl)
|
|
49
|
+
expect(url.protocol).toBe("file:")
|
|
50
|
+
expect(url.pathname).toBe("/home/user/project/README.md")
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test("should handle Linux path with special characters", () => {
|
|
54
|
+
const linuxPath = "/home/user/file#name with spaces.txt"
|
|
55
|
+
const result = encodeFilePath(linuxPath)
|
|
56
|
+
const fileUrl = `file://${result}`
|
|
57
|
+
|
|
58
|
+
expect(() => new URL(fileUrl)).not.toThrow()
|
|
59
|
+
expect(result).toBe("/home/user/file%23name%20with%20spaces.txt")
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test("should handle Linux relative path", () => {
|
|
63
|
+
const relativePath = "src/components/App.tsx"
|
|
64
|
+
const result = encodeFilePath(relativePath)
|
|
65
|
+
|
|
66
|
+
expect(result).toBe("src/components/App.tsx")
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test("should handle Linux root directory", () => {
|
|
70
|
+
const result = encodeFilePath("/")
|
|
71
|
+
expect(result).toBe("/")
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test("should handle Linux path with all special chars", () => {
|
|
75
|
+
const path = "/path/to/file#with?special%chars&more.txt"
|
|
76
|
+
const result = encodeFilePath(path)
|
|
77
|
+
const fileUrl = `file://${result}`
|
|
78
|
+
|
|
79
|
+
expect(() => new URL(fileUrl)).not.toThrow()
|
|
80
|
+
expect(result).toContain("%23") // #
|
|
81
|
+
expect(result).toContain("%3F") // ?
|
|
82
|
+
expect(result).toContain("%25") // %
|
|
83
|
+
expect(result).toContain("%26") // &
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
describe("macOS paths", () => {
|
|
88
|
+
test("should handle macOS absolute path", () => {
|
|
89
|
+
const macPath = "/Users/kelvin/Projects/opencode/README.md"
|
|
90
|
+
const result = encodeFilePath(macPath)
|
|
91
|
+
const fileUrl = `file://${result}`
|
|
92
|
+
|
|
93
|
+
expect(() => new URL(fileUrl)).not.toThrow()
|
|
94
|
+
expect(result).toBe("/Users/kelvin/Projects/opencode/README.md")
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test("should handle macOS path with spaces", () => {
|
|
98
|
+
const macPath = "/Users/kelvin/My Documents/file.txt"
|
|
99
|
+
const result = encodeFilePath(macPath)
|
|
100
|
+
const fileUrl = `file://${result}`
|
|
101
|
+
|
|
102
|
+
expect(() => new URL(fileUrl)).not.toThrow()
|
|
103
|
+
expect(result).toContain("My%20Documents")
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
describe("Windows paths", () => {
|
|
108
|
+
test("should handle Windows absolute path with backslashes", () => {
|
|
109
|
+
const windowsPath = "D:\\dev\\projects\\opencode\\README.bs.md"
|
|
110
|
+
const result = encodeFilePath(windowsPath)
|
|
111
|
+
const fileUrl = `file://${result}`
|
|
112
|
+
|
|
113
|
+
// Should create a valid, parseable URL
|
|
114
|
+
expect(() => new URL(fileUrl)).not.toThrow()
|
|
115
|
+
|
|
116
|
+
const url = new URL(fileUrl)
|
|
117
|
+
expect(url.protocol).toBe("file:")
|
|
118
|
+
expect(url.pathname).toContain("README.bs.md")
|
|
119
|
+
expect(result).toBe("/D:/dev/projects/opencode/README.bs.md")
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
test("should handle mixed separator path (Windows + Unix)", () => {
|
|
123
|
+
// This is what happens in build-request-parts.ts when concatenating paths
|
|
124
|
+
const mixedPath = "D:\\dev\\projects\\opencode/README.bs.md"
|
|
125
|
+
const result = encodeFilePath(mixedPath)
|
|
126
|
+
const fileUrl = `file://${result}`
|
|
127
|
+
|
|
128
|
+
expect(() => new URL(fileUrl)).not.toThrow()
|
|
129
|
+
expect(result).toBe("/D:/dev/projects/opencode/README.bs.md")
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
test("should handle Windows path with spaces", () => {
|
|
133
|
+
const windowsPath = "C:\\Program Files\\MyApp\\file with spaces.txt"
|
|
134
|
+
const result = encodeFilePath(windowsPath)
|
|
135
|
+
const fileUrl = `file://${result}`
|
|
136
|
+
|
|
137
|
+
expect(() => new URL(fileUrl)).not.toThrow()
|
|
138
|
+
expect(result).toContain("Program%20Files")
|
|
139
|
+
expect(result).toContain("file%20with%20spaces.txt")
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
test("should handle Windows path with special chars in filename", () => {
|
|
143
|
+
const windowsPath = "D:\\projects\\file#name with ?marks.txt"
|
|
144
|
+
const result = encodeFilePath(windowsPath)
|
|
145
|
+
const fileUrl = `file://${result}`
|
|
146
|
+
|
|
147
|
+
expect(() => new URL(fileUrl)).not.toThrow()
|
|
148
|
+
expect(result).toContain("file%23name%20with%20%3Fmarks.txt")
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
test("should handle Windows root directory", () => {
|
|
152
|
+
const windowsPath = "C:\\"
|
|
153
|
+
const result = encodeFilePath(windowsPath)
|
|
154
|
+
const fileUrl = `file://${result}`
|
|
155
|
+
|
|
156
|
+
expect(() => new URL(fileUrl)).not.toThrow()
|
|
157
|
+
expect(result).toBe("/C:/")
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
test("should handle Windows relative path with backslashes", () => {
|
|
161
|
+
const windowsPath = "src\\components\\App.tsx"
|
|
162
|
+
const result = encodeFilePath(windowsPath)
|
|
163
|
+
|
|
164
|
+
// Relative paths shouldn't get the leading slash
|
|
165
|
+
expect(result).toBe("src/components/App.tsx")
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
test("should NOT create invalid URL like the bug report", () => {
|
|
169
|
+
// This is the exact scenario from bug report by @alexyaroshuk
|
|
170
|
+
const windowsPath = "D:\\dev\\projects\\opencode\\README.bs.md"
|
|
171
|
+
const result = encodeFilePath(windowsPath)
|
|
172
|
+
const fileUrl = `file://${result}`
|
|
173
|
+
|
|
174
|
+
// The bug was creating: file://D%3A%5Cdev%5Cprojects%5Copencode/README.bs.md
|
|
175
|
+
expect(result).not.toContain("%5C") // Should not have encoded backslashes
|
|
176
|
+
expect(result).not.toBe("D%3A%5Cdev%5Cprojects%5Copencode/README.bs.md")
|
|
177
|
+
|
|
178
|
+
// Should be valid
|
|
179
|
+
expect(() => new URL(fileUrl)).not.toThrow()
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
test("should handle lowercase drive letters", () => {
|
|
183
|
+
const windowsPath = "c:\\users\\test\\file.txt"
|
|
184
|
+
const result = encodeFilePath(windowsPath)
|
|
185
|
+
const fileUrl = `file://${result}`
|
|
186
|
+
|
|
187
|
+
expect(() => new URL(fileUrl)).not.toThrow()
|
|
188
|
+
expect(result).toBe("/c:/users/test/file.txt")
|
|
189
|
+
})
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
describe("Cross-platform compatibility", () => {
|
|
193
|
+
test("should preserve Unix paths unchanged (except encoding)", () => {
|
|
194
|
+
const unixPath = "/usr/local/bin/app"
|
|
195
|
+
const result = encodeFilePath(unixPath)
|
|
196
|
+
expect(result).toBe("/usr/local/bin/app")
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
test("should normalize Windows paths for cross-platform use", () => {
|
|
200
|
+
const windowsPath = "C:\\Users\\test\\file.txt"
|
|
201
|
+
const result = encodeFilePath(windowsPath)
|
|
202
|
+
// Should convert to forward slashes and add leading /
|
|
203
|
+
expect(result).not.toContain("\\")
|
|
204
|
+
expect(result).toMatch(/^\/[A-Za-z]:\//)
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
test("should handle relative paths the same on all platforms", () => {
|
|
208
|
+
const unixRelative = "src/app.ts"
|
|
209
|
+
const windowsRelative = "src\\app.ts"
|
|
210
|
+
|
|
211
|
+
const unixResult = encodeFilePath(unixRelative)
|
|
212
|
+
const windowsResult = encodeFilePath(windowsRelative)
|
|
213
|
+
|
|
214
|
+
// Both should normalize to forward slashes
|
|
215
|
+
expect(unixResult).toBe("src/app.ts")
|
|
216
|
+
expect(windowsResult).toBe("src/app.ts")
|
|
217
|
+
})
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
describe("Edge cases", () => {
|
|
221
|
+
test("should handle empty path", () => {
|
|
222
|
+
const result = encodeFilePath("")
|
|
223
|
+
expect(result).toBe("")
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
test("should handle path with multiple consecutive slashes", () => {
|
|
227
|
+
const result = encodeFilePath("//path//to///file.txt")
|
|
228
|
+
// Multiple slashes should be preserved (backend handles normalization)
|
|
229
|
+
expect(result).toBe("//path//to///file.txt")
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
test("should encode Unicode characters", () => {
|
|
233
|
+
const unicodePath = "/home/user/文档/README.md"
|
|
234
|
+
const result = encodeFilePath(unicodePath)
|
|
235
|
+
const fileUrl = `file://${result}`
|
|
236
|
+
|
|
237
|
+
expect(() => new URL(fileUrl)).not.toThrow()
|
|
238
|
+
// Unicode should be encoded
|
|
239
|
+
expect(result).toContain("%E6%96%87%E6%A1%A3")
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
test("should handle already normalized Windows path", () => {
|
|
243
|
+
// Path that's already been normalized (has / before drive letter)
|
|
244
|
+
const alreadyNormalized = "/D:/path/file.txt"
|
|
245
|
+
const result = encodeFilePath(alreadyNormalized)
|
|
246
|
+
|
|
247
|
+
// Should not add another leading slash
|
|
248
|
+
expect(result).toBe("/D:/path/file.txt")
|
|
249
|
+
expect(result).not.toContain("//D")
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
test("should handle just drive letter", () => {
|
|
253
|
+
const justDrive = "D:"
|
|
254
|
+
const result = encodeFilePath(justDrive)
|
|
255
|
+
const fileUrl = `file://${result}`
|
|
256
|
+
|
|
257
|
+
expect(result).toBe("/D:")
|
|
258
|
+
expect(() => new URL(fileUrl)).not.toThrow()
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
test("should handle Windows path with trailing backslash", () => {
|
|
262
|
+
const trailingBackslash = "C:\\Users\\test\\"
|
|
263
|
+
const result = encodeFilePath(trailingBackslash)
|
|
264
|
+
const fileUrl = `file://${result}`
|
|
265
|
+
|
|
266
|
+
expect(() => new URL(fileUrl)).not.toThrow()
|
|
267
|
+
expect(result).toBe("/C:/Users/test/")
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
test("should handle very long paths", () => {
|
|
271
|
+
const longPath = "C:\\Users\\test\\" + "verylongdirectoryname\\".repeat(20) + "file.txt"
|
|
272
|
+
const result = encodeFilePath(longPath)
|
|
273
|
+
const fileUrl = `file://${result}`
|
|
274
|
+
|
|
275
|
+
expect(() => new URL(fileUrl)).not.toThrow()
|
|
276
|
+
expect(result).not.toContain("\\")
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
test("should handle paths with dots", () => {
|
|
280
|
+
const pathWithDots = "C:\\Users\\..\\test\\.\\file.txt"
|
|
281
|
+
const result = encodeFilePath(pathWithDots)
|
|
282
|
+
const fileUrl = `file://${result}`
|
|
283
|
+
|
|
284
|
+
expect(() => new URL(fileUrl)).not.toThrow()
|
|
285
|
+
// Dots should be preserved (backend normalizes)
|
|
286
|
+
expect(result).toContain("..")
|
|
287
|
+
expect(result).toContain("/./")
|
|
288
|
+
})
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
describe("Regression tests for PR #12424", () => {
|
|
292
|
+
test("should handle file with # in name", () => {
|
|
293
|
+
const path = "/path/to/file#name.txt"
|
|
294
|
+
const result = encodeFilePath(path)
|
|
295
|
+
const fileUrl = `file://${result}`
|
|
296
|
+
|
|
297
|
+
expect(() => new URL(fileUrl)).not.toThrow()
|
|
298
|
+
expect(result).toBe("/path/to/file%23name.txt")
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
test("should handle file with ? in name", () => {
|
|
302
|
+
const path = "/path/to/file?name.txt"
|
|
303
|
+
const result = encodeFilePath(path)
|
|
304
|
+
const fileUrl = `file://${result}`
|
|
305
|
+
|
|
306
|
+
expect(() => new URL(fileUrl)).not.toThrow()
|
|
307
|
+
expect(result).toBe("/path/to/file%3Fname.txt")
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
test("should handle file with % in name", () => {
|
|
311
|
+
const path = "/path/to/file%name.txt"
|
|
312
|
+
const result = encodeFilePath(path)
|
|
313
|
+
const fileUrl = `file://${result}`
|
|
314
|
+
|
|
315
|
+
expect(() => new URL(fileUrl)).not.toThrow()
|
|
316
|
+
expect(result).toBe("/path/to/file%25name.txt")
|
|
317
|
+
})
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
describe("Integration with file:// URL construction", () => {
|
|
321
|
+
test("should work with query parameters (Linux)", () => {
|
|
322
|
+
const path = "/home/user/file.txt"
|
|
323
|
+
const encoded = encodeFilePath(path)
|
|
324
|
+
const fileUrl = `file://${encoded}?start=10&end=20`
|
|
325
|
+
|
|
326
|
+
const url = new URL(fileUrl)
|
|
327
|
+
expect(url.searchParams.get("start")).toBe("10")
|
|
328
|
+
expect(url.searchParams.get("end")).toBe("20")
|
|
329
|
+
expect(url.pathname).toBe("/home/user/file.txt")
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
test("should work with query parameters (Windows)", () => {
|
|
333
|
+
const path = "C:\\Users\\test\\file.txt"
|
|
334
|
+
const encoded = encodeFilePath(path)
|
|
335
|
+
const fileUrl = `file://${encoded}?start=10&end=20`
|
|
336
|
+
|
|
337
|
+
const url = new URL(fileUrl)
|
|
338
|
+
expect(url.searchParams.get("start")).toBe("10")
|
|
339
|
+
expect(url.searchParams.get("end")).toBe("20")
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
test("should parse correctly in URL constructor (Linux)", () => {
|
|
343
|
+
const path = "/var/log/app.log"
|
|
344
|
+
const fileUrl = `file://${encodeFilePath(path)}`
|
|
345
|
+
const url = new URL(fileUrl)
|
|
346
|
+
|
|
347
|
+
expect(url.protocol).toBe("file:")
|
|
348
|
+
expect(url.pathname).toBe("/var/log/app.log")
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
test("should parse correctly in URL constructor (Windows)", () => {
|
|
352
|
+
const path = "D:\\logs\\app.log"
|
|
353
|
+
const fileUrl = `file://${encodeFilePath(path)}`
|
|
354
|
+
const url = new URL(fileUrl)
|
|
355
|
+
|
|
356
|
+
expect(url.protocol).toBe("file:")
|
|
357
|
+
expect(url.pathname).toContain("app.log")
|
|
358
|
+
})
|
|
359
|
+
})
|
|
360
|
+
})
|