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,476 @@
|
|
|
1
|
+
import { Platform, usePlatform } from "@/context/platform"
|
|
2
|
+
import { makePersisted, type AsyncStorage, type SyncStorage } from "@solid-primitives/storage"
|
|
3
|
+
import { checksum } from "@reign-labs/util/encode"
|
|
4
|
+
import { createResource, type Accessor } from "solid-js"
|
|
5
|
+
import type { SetStoreFunction, Store } from "solid-js/store"
|
|
6
|
+
|
|
7
|
+
type InitType = Promise<string> | string | null
|
|
8
|
+
type PersistedWithReady<T> = [
|
|
9
|
+
Store<T>,
|
|
10
|
+
SetStoreFunction<T>,
|
|
11
|
+
InitType,
|
|
12
|
+
Accessor<boolean> & { promise: undefined | Promise<any> },
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
type PersistTarget = {
|
|
16
|
+
storage?: string
|
|
17
|
+
key: string
|
|
18
|
+
legacy?: string[]
|
|
19
|
+
migrate?: (value: unknown) => unknown
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const LEGACY_STORAGE = "default.dat"
|
|
23
|
+
const GLOBAL_STORAGE = "opencode.global.dat"
|
|
24
|
+
const LOCAL_PREFIX = "opencode."
|
|
25
|
+
const fallback = new Map<string, boolean>()
|
|
26
|
+
|
|
27
|
+
const CACHE_MAX_ENTRIES = 500
|
|
28
|
+
const CACHE_MAX_BYTES = 8 * 1024 * 1024
|
|
29
|
+
|
|
30
|
+
type CacheEntry = { value: string; bytes: number }
|
|
31
|
+
const cache = new Map<string, CacheEntry>()
|
|
32
|
+
const cacheTotal = { bytes: 0 }
|
|
33
|
+
|
|
34
|
+
function cacheDelete(key: string) {
|
|
35
|
+
const entry = cache.get(key)
|
|
36
|
+
if (!entry) return
|
|
37
|
+
cacheTotal.bytes -= entry.bytes
|
|
38
|
+
cache.delete(key)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function cachePrune() {
|
|
42
|
+
for (;;) {
|
|
43
|
+
if (cache.size <= CACHE_MAX_ENTRIES && cacheTotal.bytes <= CACHE_MAX_BYTES) return
|
|
44
|
+
const oldest = cache.keys().next().value as string | undefined
|
|
45
|
+
if (!oldest) return
|
|
46
|
+
cacheDelete(oldest)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function cacheSet(key: string, value: string) {
|
|
51
|
+
const bytes = value.length * 2
|
|
52
|
+
if (bytes > CACHE_MAX_BYTES) {
|
|
53
|
+
cacheDelete(key)
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const entry = cache.get(key)
|
|
58
|
+
if (entry) cacheTotal.bytes -= entry.bytes
|
|
59
|
+
cache.delete(key)
|
|
60
|
+
cache.set(key, { value, bytes })
|
|
61
|
+
cacheTotal.bytes += bytes
|
|
62
|
+
cachePrune()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function cacheGet(key: string) {
|
|
66
|
+
const entry = cache.get(key)
|
|
67
|
+
if (!entry) return
|
|
68
|
+
cache.delete(key)
|
|
69
|
+
cache.set(key, entry)
|
|
70
|
+
return entry.value
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function fallbackDisabled(scope: string) {
|
|
74
|
+
return fallback.get(scope) === true
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function fallbackSet(scope: string) {
|
|
78
|
+
fallback.set(scope, true)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function quota(error: unknown) {
|
|
82
|
+
if (error instanceof DOMException) {
|
|
83
|
+
if (error.name === "QuotaExceededError") return true
|
|
84
|
+
if (error.name === "NS_ERROR_DOM_QUOTA_REACHED") return true
|
|
85
|
+
if (error.name === "QUOTA_EXCEEDED_ERR") return true
|
|
86
|
+
if (error.code === 22 || error.code === 1014) return true
|
|
87
|
+
return false
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!error || typeof error !== "object") return false
|
|
91
|
+
const name = (error as { name?: string }).name
|
|
92
|
+
if (name === "QuotaExceededError" || name === "NS_ERROR_DOM_QUOTA_REACHED") return true
|
|
93
|
+
if (name && /quota/i.test(name)) return true
|
|
94
|
+
|
|
95
|
+
const code = (error as { code?: number }).code
|
|
96
|
+
if (code === 22 || code === 1014) return true
|
|
97
|
+
|
|
98
|
+
const message = (error as { message?: string }).message
|
|
99
|
+
if (typeof message !== "string") return false
|
|
100
|
+
if (/quota/i.test(message)) return true
|
|
101
|
+
return false
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
type Evict = { key: string; size: number }
|
|
105
|
+
|
|
106
|
+
function evict(storage: Storage, keep: string, value: string) {
|
|
107
|
+
const total = storage.length
|
|
108
|
+
const indexes = Array.from({ length: total }, (_, index) => index)
|
|
109
|
+
const items: Evict[] = []
|
|
110
|
+
|
|
111
|
+
for (const index of indexes) {
|
|
112
|
+
const name = storage.key(index)
|
|
113
|
+
if (!name) continue
|
|
114
|
+
if (!name.startsWith(LOCAL_PREFIX)) continue
|
|
115
|
+
if (name === keep) continue
|
|
116
|
+
const stored = storage.getItem(name)
|
|
117
|
+
items.push({ key: name, size: stored?.length ?? 0 })
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
items.sort((a, b) => b.size - a.size)
|
|
121
|
+
|
|
122
|
+
for (const item of items) {
|
|
123
|
+
storage.removeItem(item.key)
|
|
124
|
+
cacheDelete(item.key)
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
storage.setItem(keep, value)
|
|
128
|
+
cacheSet(keep, value)
|
|
129
|
+
return true
|
|
130
|
+
} catch (error) {
|
|
131
|
+
if (!quota(error)) throw error
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return false
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function write(storage: Storage, key: string, value: string) {
|
|
139
|
+
try {
|
|
140
|
+
storage.setItem(key, value)
|
|
141
|
+
cacheSet(key, value)
|
|
142
|
+
return true
|
|
143
|
+
} catch (error) {
|
|
144
|
+
if (!quota(error)) throw error
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
storage.removeItem(key)
|
|
149
|
+
cacheDelete(key)
|
|
150
|
+
storage.setItem(key, value)
|
|
151
|
+
cacheSet(key, value)
|
|
152
|
+
return true
|
|
153
|
+
} catch (error) {
|
|
154
|
+
if (!quota(error)) throw error
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const ok = evict(storage, key, value)
|
|
158
|
+
return ok
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function snapshot(value: unknown) {
|
|
162
|
+
return JSON.parse(JSON.stringify(value)) as unknown
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
166
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function merge(defaults: unknown, value: unknown): unknown {
|
|
170
|
+
if (value === undefined) return defaults
|
|
171
|
+
if (value === null) return value
|
|
172
|
+
|
|
173
|
+
if (Array.isArray(defaults)) {
|
|
174
|
+
if (Array.isArray(value)) return value
|
|
175
|
+
return defaults
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (isRecord(defaults)) {
|
|
179
|
+
if (!isRecord(value)) return defaults
|
|
180
|
+
|
|
181
|
+
const result: Record<string, unknown> = { ...defaults }
|
|
182
|
+
for (const key of Object.keys(value)) {
|
|
183
|
+
if (key in defaults) {
|
|
184
|
+
result[key] = merge((defaults as Record<string, unknown>)[key], (value as Record<string, unknown>)[key])
|
|
185
|
+
} else {
|
|
186
|
+
result[key] = (value as Record<string, unknown>)[key]
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return result
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return value
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function parse(value: string) {
|
|
196
|
+
try {
|
|
197
|
+
return JSON.parse(value) as unknown
|
|
198
|
+
} catch {
|
|
199
|
+
return undefined
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function normalize(defaults: unknown, raw: string, migrate?: (value: unknown) => unknown) {
|
|
204
|
+
const parsed = parse(raw)
|
|
205
|
+
if (parsed === undefined) return
|
|
206
|
+
const migrated = migrate ? migrate(parsed) : parsed
|
|
207
|
+
const merged = merge(defaults, migrated)
|
|
208
|
+
return JSON.stringify(merged)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function workspaceStorage(dir: string) {
|
|
212
|
+
const head = (dir.slice(0, 12) || "workspace").replace(/[^a-zA-Z0-9._-]/g, "-")
|
|
213
|
+
const sum = checksum(dir) ?? "0"
|
|
214
|
+
return `opencode.workspace.${head}.${sum}.dat`
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function localStorageWithPrefix(prefix: string): SyncStorage {
|
|
218
|
+
const base = `${prefix}:`
|
|
219
|
+
const scope = `prefix:${prefix}`
|
|
220
|
+
const item = (key: string) => base + key
|
|
221
|
+
return {
|
|
222
|
+
getItem: (key) => {
|
|
223
|
+
const name = item(key)
|
|
224
|
+
const cached = cacheGet(name)
|
|
225
|
+
if (fallbackDisabled(scope)) return cached ?? null
|
|
226
|
+
|
|
227
|
+
const stored = (() => {
|
|
228
|
+
try {
|
|
229
|
+
return localStorage.getItem(name)
|
|
230
|
+
} catch {
|
|
231
|
+
fallbackSet(scope)
|
|
232
|
+
return null
|
|
233
|
+
}
|
|
234
|
+
})()
|
|
235
|
+
if (stored === null) return cached ?? null
|
|
236
|
+
cacheSet(name, stored)
|
|
237
|
+
return stored
|
|
238
|
+
},
|
|
239
|
+
setItem: (key, value) => {
|
|
240
|
+
const name = item(key)
|
|
241
|
+
if (fallbackDisabled(scope)) return
|
|
242
|
+
try {
|
|
243
|
+
if (write(localStorage, name, value)) return
|
|
244
|
+
} catch {
|
|
245
|
+
fallbackSet(scope)
|
|
246
|
+
return
|
|
247
|
+
}
|
|
248
|
+
fallbackSet(scope)
|
|
249
|
+
},
|
|
250
|
+
removeItem: (key) => {
|
|
251
|
+
const name = item(key)
|
|
252
|
+
cacheDelete(name)
|
|
253
|
+
if (fallbackDisabled(scope)) return
|
|
254
|
+
try {
|
|
255
|
+
localStorage.removeItem(name)
|
|
256
|
+
} catch {
|
|
257
|
+
fallbackSet(scope)
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function localStorageDirect(): SyncStorage {
|
|
264
|
+
const scope = "direct"
|
|
265
|
+
return {
|
|
266
|
+
getItem: (key) => {
|
|
267
|
+
const cached = cacheGet(key)
|
|
268
|
+
if (fallbackDisabled(scope)) return cached ?? null
|
|
269
|
+
|
|
270
|
+
const stored = (() => {
|
|
271
|
+
try {
|
|
272
|
+
return localStorage.getItem(key)
|
|
273
|
+
} catch {
|
|
274
|
+
fallbackSet(scope)
|
|
275
|
+
return null
|
|
276
|
+
}
|
|
277
|
+
})()
|
|
278
|
+
if (stored === null) return cached ?? null
|
|
279
|
+
cacheSet(key, stored)
|
|
280
|
+
return stored
|
|
281
|
+
},
|
|
282
|
+
setItem: (key, value) => {
|
|
283
|
+
if (fallbackDisabled(scope)) return
|
|
284
|
+
try {
|
|
285
|
+
if (write(localStorage, key, value)) return
|
|
286
|
+
} catch {
|
|
287
|
+
fallbackSet(scope)
|
|
288
|
+
return
|
|
289
|
+
}
|
|
290
|
+
fallbackSet(scope)
|
|
291
|
+
},
|
|
292
|
+
removeItem: (key) => {
|
|
293
|
+
cacheDelete(key)
|
|
294
|
+
if (fallbackDisabled(scope)) return
|
|
295
|
+
try {
|
|
296
|
+
localStorage.removeItem(key)
|
|
297
|
+
} catch {
|
|
298
|
+
fallbackSet(scope)
|
|
299
|
+
}
|
|
300
|
+
},
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export const PersistTesting = {
|
|
305
|
+
localStorageDirect,
|
|
306
|
+
localStorageWithPrefix,
|
|
307
|
+
normalize,
|
|
308
|
+
workspaceStorage,
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export const Persist = {
|
|
312
|
+
global(key: string, legacy?: string[]): PersistTarget {
|
|
313
|
+
return { storage: GLOBAL_STORAGE, key, legacy }
|
|
314
|
+
},
|
|
315
|
+
workspace(dir: string, key: string, legacy?: string[]): PersistTarget {
|
|
316
|
+
return { storage: workspaceStorage(dir), key: `workspace:${key}`, legacy }
|
|
317
|
+
},
|
|
318
|
+
session(dir: string, session: string, key: string, legacy?: string[]): PersistTarget {
|
|
319
|
+
return { storage: workspaceStorage(dir), key: `session:${session}:${key}`, legacy }
|
|
320
|
+
},
|
|
321
|
+
scoped(dir: string, session: string | undefined, key: string, legacy?: string[]): PersistTarget {
|
|
322
|
+
if (session) return Persist.session(dir, session, key, legacy)
|
|
323
|
+
return Persist.workspace(dir, key, legacy)
|
|
324
|
+
},
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export function removePersisted(target: { storage?: string; key: string }, platform?: Platform) {
|
|
328
|
+
const isDesktop = platform?.platform === "desktop" && !!platform.storage
|
|
329
|
+
|
|
330
|
+
if (isDesktop) {
|
|
331
|
+
return platform.storage?.(target.storage)?.removeItem(target.key)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (!target.storage) {
|
|
335
|
+
localStorageDirect().removeItem(target.key)
|
|
336
|
+
return
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
localStorageWithPrefix(target.storage).removeItem(target.key)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export function persisted<T>(
|
|
343
|
+
target: string | PersistTarget,
|
|
344
|
+
store: [Store<T>, SetStoreFunction<T>],
|
|
345
|
+
): PersistedWithReady<T> {
|
|
346
|
+
const platform = usePlatform()
|
|
347
|
+
const config: PersistTarget = typeof target === "string" ? { key: target } : target
|
|
348
|
+
|
|
349
|
+
const defaults = snapshot(store[0])
|
|
350
|
+
const legacy = config.legacy ?? []
|
|
351
|
+
|
|
352
|
+
const isDesktop = platform.platform === "desktop" && !!platform.storage
|
|
353
|
+
|
|
354
|
+
const currentStorage = (() => {
|
|
355
|
+
if (isDesktop) return platform.storage?.(config.storage)
|
|
356
|
+
if (!config.storage) return localStorageDirect()
|
|
357
|
+
return localStorageWithPrefix(config.storage)
|
|
358
|
+
})()
|
|
359
|
+
|
|
360
|
+
const legacyStorage = (() => {
|
|
361
|
+
if (!isDesktop) return localStorageDirect()
|
|
362
|
+
if (!config.storage) return platform.storage?.()
|
|
363
|
+
return platform.storage?.(LEGACY_STORAGE)
|
|
364
|
+
})()
|
|
365
|
+
|
|
366
|
+
const storage = (() => {
|
|
367
|
+
if (!isDesktop) {
|
|
368
|
+
const current = currentStorage as SyncStorage
|
|
369
|
+
const legacyStore = legacyStorage as SyncStorage
|
|
370
|
+
|
|
371
|
+
const api: SyncStorage = {
|
|
372
|
+
getItem: (key) => {
|
|
373
|
+
const raw = current.getItem(key)
|
|
374
|
+
if (raw !== null) {
|
|
375
|
+
const next = normalize(defaults, raw, config.migrate)
|
|
376
|
+
if (next === undefined) {
|
|
377
|
+
current.removeItem(key)
|
|
378
|
+
return null
|
|
379
|
+
}
|
|
380
|
+
if (raw !== next) current.setItem(key, next)
|
|
381
|
+
return next
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
for (const legacyKey of legacy) {
|
|
385
|
+
const legacyRaw = legacyStore.getItem(legacyKey)
|
|
386
|
+
if (legacyRaw === null) continue
|
|
387
|
+
|
|
388
|
+
const next = normalize(defaults, legacyRaw, config.migrate)
|
|
389
|
+
if (next === undefined) {
|
|
390
|
+
legacyStore.removeItem(legacyKey)
|
|
391
|
+
continue
|
|
392
|
+
}
|
|
393
|
+
current.setItem(key, next)
|
|
394
|
+
legacyStore.removeItem(legacyKey)
|
|
395
|
+
return next
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return null
|
|
399
|
+
},
|
|
400
|
+
setItem: (key, value) => {
|
|
401
|
+
current.setItem(key, value)
|
|
402
|
+
},
|
|
403
|
+
removeItem: (key) => {
|
|
404
|
+
current.removeItem(key)
|
|
405
|
+
},
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return api
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const current = currentStorage as AsyncStorage
|
|
412
|
+
const legacyStore = legacyStorage as AsyncStorage | undefined
|
|
413
|
+
|
|
414
|
+
const api: AsyncStorage = {
|
|
415
|
+
getItem: async (key) => {
|
|
416
|
+
const raw = await current.getItem(key)
|
|
417
|
+
if (raw !== null) {
|
|
418
|
+
const next = normalize(defaults, raw, config.migrate)
|
|
419
|
+
if (next === undefined) {
|
|
420
|
+
await current.removeItem(key).catch(() => undefined)
|
|
421
|
+
return null
|
|
422
|
+
}
|
|
423
|
+
if (raw !== next) await current.setItem(key, next)
|
|
424
|
+
return next
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (!legacyStore) return null
|
|
428
|
+
|
|
429
|
+
for (const legacyKey of legacy) {
|
|
430
|
+
const legacyRaw = await legacyStore.getItem(legacyKey)
|
|
431
|
+
if (legacyRaw === null) continue
|
|
432
|
+
|
|
433
|
+
const next = normalize(defaults, legacyRaw, config.migrate)
|
|
434
|
+
if (next === undefined) {
|
|
435
|
+
await legacyStore.removeItem(legacyKey).catch(() => undefined)
|
|
436
|
+
continue
|
|
437
|
+
}
|
|
438
|
+
await current.setItem(key, next)
|
|
439
|
+
await legacyStore.removeItem(legacyKey)
|
|
440
|
+
return next
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return null
|
|
444
|
+
},
|
|
445
|
+
setItem: async (key, value) => {
|
|
446
|
+
await current.setItem(key, value)
|
|
447
|
+
},
|
|
448
|
+
removeItem: async (key) => {
|
|
449
|
+
await current.removeItem(key)
|
|
450
|
+
},
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return api
|
|
454
|
+
})()
|
|
455
|
+
|
|
456
|
+
const [state, setState, init] = makePersisted(store, { name: config.key, storage })
|
|
457
|
+
|
|
458
|
+
const isAsync = init instanceof Promise
|
|
459
|
+
const [ready] = createResource(
|
|
460
|
+
() => init,
|
|
461
|
+
async (initValue) => {
|
|
462
|
+
if (initValue instanceof Promise) await initValue
|
|
463
|
+
return true
|
|
464
|
+
},
|
|
465
|
+
{ initialValue: !isAsync },
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
return [
|
|
469
|
+
state,
|
|
470
|
+
setState,
|
|
471
|
+
init,
|
|
472
|
+
Object.assign(() => ready() === true, {
|
|
473
|
+
promise: init instanceof Promise ? init : undefined,
|
|
474
|
+
}),
|
|
475
|
+
]
|
|
476
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import type { Part } from "@reign-labs/sdk/v2"
|
|
3
|
+
import { extractPromptFromParts } from "./prompt"
|
|
4
|
+
|
|
5
|
+
describe("extractPromptFromParts", () => {
|
|
6
|
+
test("restores multiple uploaded attachments", () => {
|
|
7
|
+
const parts = [
|
|
8
|
+
{
|
|
9
|
+
id: "text_1",
|
|
10
|
+
type: "text",
|
|
11
|
+
text: "check these",
|
|
12
|
+
sessionID: "ses_1",
|
|
13
|
+
messageID: "msg_1",
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
id: "file_1",
|
|
17
|
+
type: "file",
|
|
18
|
+
mime: "image/png",
|
|
19
|
+
url: "data:image/png;base64,AAA",
|
|
20
|
+
filename: "a.png",
|
|
21
|
+
sessionID: "ses_1",
|
|
22
|
+
messageID: "msg_1",
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: "file_2",
|
|
26
|
+
type: "file",
|
|
27
|
+
mime: "application/pdf",
|
|
28
|
+
url: "data:application/pdf;base64,BBB",
|
|
29
|
+
filename: "b.pdf",
|
|
30
|
+
sessionID: "ses_1",
|
|
31
|
+
messageID: "msg_1",
|
|
32
|
+
},
|
|
33
|
+
] satisfies Part[]
|
|
34
|
+
|
|
35
|
+
const result = extractPromptFromParts(parts)
|
|
36
|
+
|
|
37
|
+
expect(result).toHaveLength(3)
|
|
38
|
+
expect(result[0]).toMatchObject({ type: "text", content: "check these" })
|
|
39
|
+
expect(result.slice(1)).toMatchObject([
|
|
40
|
+
{ type: "image", filename: "a.png", mime: "image/png", dataUrl: "data:image/png;base64,AAA" },
|
|
41
|
+
{ type: "image", filename: "b.pdf", mime: "application/pdf", dataUrl: "data:application/pdf;base64,BBB" },
|
|
42
|
+
])
|
|
43
|
+
})
|
|
44
|
+
})
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import type { AgentPart as MessageAgentPart, FilePart, Part, TextPart } from "@reign-labs/sdk/v2"
|
|
2
|
+
import type { AgentPart, FileAttachmentPart, ImageAttachmentPart, Prompt } from "@/context/prompt"
|
|
3
|
+
|
|
4
|
+
type Inline =
|
|
5
|
+
| {
|
|
6
|
+
type: "file"
|
|
7
|
+
start: number
|
|
8
|
+
end: number
|
|
9
|
+
value: string
|
|
10
|
+
path: string
|
|
11
|
+
selection?: {
|
|
12
|
+
startLine: number
|
|
13
|
+
endLine: number
|
|
14
|
+
startChar: number
|
|
15
|
+
endChar: number
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
| {
|
|
19
|
+
type: "agent"
|
|
20
|
+
start: number
|
|
21
|
+
end: number
|
|
22
|
+
value: string
|
|
23
|
+
name: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function selectionFromFileUrl(url: string): Extract<Inline, { type: "file" }>["selection"] {
|
|
27
|
+
const queryIndex = url.indexOf("?")
|
|
28
|
+
if (queryIndex === -1) return undefined
|
|
29
|
+
const params = new URLSearchParams(url.slice(queryIndex + 1))
|
|
30
|
+
const startLine = Number(params.get("start"))
|
|
31
|
+
const endLine = Number(params.get("end"))
|
|
32
|
+
if (!Number.isFinite(startLine) || !Number.isFinite(endLine)) return undefined
|
|
33
|
+
return {
|
|
34
|
+
startLine,
|
|
35
|
+
endLine,
|
|
36
|
+
startChar: 0,
|
|
37
|
+
endChar: 0,
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function textPartValue(parts: Part[]) {
|
|
42
|
+
const candidates = parts
|
|
43
|
+
.filter((part): part is TextPart => part.type === "text")
|
|
44
|
+
.filter((part) => !part.synthetic && !part.ignored)
|
|
45
|
+
return candidates.reduce((best: TextPart | undefined, part) => {
|
|
46
|
+
if (!best) return part
|
|
47
|
+
if (part.text.length > best.text.length) return part
|
|
48
|
+
return best
|
|
49
|
+
}, undefined)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Extract prompt content from message parts for restoring into the prompt input.
|
|
54
|
+
* This is used by undo to restore the original user prompt.
|
|
55
|
+
*/
|
|
56
|
+
export function extractPromptFromParts(parts: Part[], opts?: { directory?: string; attachmentName?: string }): Prompt {
|
|
57
|
+
const textPart = textPartValue(parts)
|
|
58
|
+
const text = textPart?.text ?? ""
|
|
59
|
+
const directory = opts?.directory
|
|
60
|
+
const attachmentName = opts?.attachmentName ?? "attachment"
|
|
61
|
+
|
|
62
|
+
const toRelative = (path: string) => {
|
|
63
|
+
if (!directory) return path
|
|
64
|
+
|
|
65
|
+
const prefix = directory.endsWith("/") ? directory : directory + "/"
|
|
66
|
+
if (path.startsWith(prefix)) return path.slice(prefix.length)
|
|
67
|
+
|
|
68
|
+
if (path.startsWith(directory)) {
|
|
69
|
+
const next = path.slice(directory.length)
|
|
70
|
+
if (next.startsWith("/")) return next.slice(1)
|
|
71
|
+
return next
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return path
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const inline: Inline[] = []
|
|
78
|
+
const images: ImageAttachmentPart[] = []
|
|
79
|
+
|
|
80
|
+
for (const part of parts) {
|
|
81
|
+
if (part.type === "file") {
|
|
82
|
+
const filePart = part as FilePart
|
|
83
|
+
const sourceText = filePart.source?.text
|
|
84
|
+
if (sourceText) {
|
|
85
|
+
const value = sourceText.value
|
|
86
|
+
const start = sourceText.start
|
|
87
|
+
const end = sourceText.end
|
|
88
|
+
let path = value
|
|
89
|
+
if (value.startsWith("@")) path = value.slice(1)
|
|
90
|
+
if (!value.startsWith("@") && filePart.source && "path" in filePart.source) {
|
|
91
|
+
path = filePart.source.path
|
|
92
|
+
}
|
|
93
|
+
inline.push({
|
|
94
|
+
type: "file",
|
|
95
|
+
start,
|
|
96
|
+
end,
|
|
97
|
+
value,
|
|
98
|
+
path: toRelative(path),
|
|
99
|
+
selection: selectionFromFileUrl(filePart.url),
|
|
100
|
+
})
|
|
101
|
+
continue
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (filePart.url.startsWith("data:")) {
|
|
105
|
+
images.push({
|
|
106
|
+
type: "image",
|
|
107
|
+
id: filePart.id,
|
|
108
|
+
filename: filePart.filename ?? attachmentName,
|
|
109
|
+
mime: filePart.mime,
|
|
110
|
+
dataUrl: filePart.url,
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (part.type === "agent") {
|
|
116
|
+
const agentPart = part as MessageAgentPart
|
|
117
|
+
const source = agentPart.source
|
|
118
|
+
if (!source) continue
|
|
119
|
+
inline.push({
|
|
120
|
+
type: "agent",
|
|
121
|
+
start: source.start,
|
|
122
|
+
end: source.end,
|
|
123
|
+
value: source.value,
|
|
124
|
+
name: agentPart.name,
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
inline.sort((a, b) => {
|
|
130
|
+
if (a.start !== b.start) return a.start - b.start
|
|
131
|
+
return a.end - b.end
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
const result: Prompt = []
|
|
135
|
+
let position = 0
|
|
136
|
+
let cursor = 0
|
|
137
|
+
|
|
138
|
+
const pushText = (content: string) => {
|
|
139
|
+
if (!content) return
|
|
140
|
+
result.push({
|
|
141
|
+
type: "text",
|
|
142
|
+
content,
|
|
143
|
+
start: position,
|
|
144
|
+
end: position + content.length,
|
|
145
|
+
})
|
|
146
|
+
position += content.length
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const pushFile = (item: Extract<Inline, { type: "file" }>) => {
|
|
150
|
+
const content = item.value
|
|
151
|
+
const attachment: FileAttachmentPart = {
|
|
152
|
+
type: "file",
|
|
153
|
+
path: item.path,
|
|
154
|
+
content,
|
|
155
|
+
start: position,
|
|
156
|
+
end: position + content.length,
|
|
157
|
+
selection: item.selection,
|
|
158
|
+
}
|
|
159
|
+
result.push(attachment)
|
|
160
|
+
position += content.length
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const pushAgent = (item: Extract<Inline, { type: "agent" }>) => {
|
|
164
|
+
const content = item.value
|
|
165
|
+
const mention: AgentPart = {
|
|
166
|
+
type: "agent",
|
|
167
|
+
name: item.name,
|
|
168
|
+
content,
|
|
169
|
+
start: position,
|
|
170
|
+
end: position + content.length,
|
|
171
|
+
}
|
|
172
|
+
result.push(mention)
|
|
173
|
+
position += content.length
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
for (const item of inline) {
|
|
177
|
+
if (item.start < 0 || item.end < item.start) continue
|
|
178
|
+
|
|
179
|
+
const expected = item.value
|
|
180
|
+
if (!expected) continue
|
|
181
|
+
|
|
182
|
+
const mismatch = item.end > text.length || item.start < cursor || text.slice(item.start, item.end) !== expected
|
|
183
|
+
const start = mismatch ? text.indexOf(expected, cursor) : item.start
|
|
184
|
+
if (start === -1) continue
|
|
185
|
+
const end = mismatch ? start + expected.length : item.end
|
|
186
|
+
|
|
187
|
+
pushText(text.slice(cursor, start))
|
|
188
|
+
|
|
189
|
+
if (item.type === "file") pushFile(item)
|
|
190
|
+
if (item.type === "agent") pushAgent(item)
|
|
191
|
+
|
|
192
|
+
cursor = end
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
pushText(text.slice(cursor))
|
|
196
|
+
|
|
197
|
+
if (result.length === 0) {
|
|
198
|
+
result.push({ type: "text", content: "", start: 0, end: 0 })
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (images.length === 0) return result
|
|
202
|
+
return [...result, ...images]
|
|
203
|
+
}
|