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 { usePlatform } from "@/context/platform"
|
|
2
|
+
import type { ServerConnection } from "@/context/server"
|
|
3
|
+
import { createSdkForServer } from "./server"
|
|
4
|
+
|
|
5
|
+
export type ServerHealth = { healthy: boolean; version?: string }
|
|
6
|
+
|
|
7
|
+
interface CheckServerHealthOptions {
|
|
8
|
+
timeoutMs?: number
|
|
9
|
+
signal?: AbortSignal
|
|
10
|
+
retryCount?: number
|
|
11
|
+
retryDelayMs?: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const defaultTimeoutMs = 3000
|
|
15
|
+
const defaultRetryCount = 2
|
|
16
|
+
const defaultRetryDelayMs = 100
|
|
17
|
+
|
|
18
|
+
function timeoutSignal(timeoutMs: number) {
|
|
19
|
+
const timeout = (AbortSignal as unknown as { timeout?: (ms: number) => AbortSignal }).timeout
|
|
20
|
+
if (timeout) {
|
|
21
|
+
try {
|
|
22
|
+
return {
|
|
23
|
+
signal: timeout.call(AbortSignal, timeoutMs),
|
|
24
|
+
clear: undefined as (() => void) | undefined,
|
|
25
|
+
}
|
|
26
|
+
} catch {}
|
|
27
|
+
}
|
|
28
|
+
const controller = new AbortController()
|
|
29
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs)
|
|
30
|
+
return { signal: controller.signal, clear: () => clearTimeout(timer) }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function wait(ms: number, signal?: AbortSignal) {
|
|
34
|
+
return new Promise<void>((resolve, reject) => {
|
|
35
|
+
if (signal?.aborted) {
|
|
36
|
+
reject(new DOMException("Aborted", "AbortError"))
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
const timer = setTimeout(() => {
|
|
40
|
+
signal?.removeEventListener("abort", onAbort)
|
|
41
|
+
resolve()
|
|
42
|
+
}, ms)
|
|
43
|
+
const onAbort = () => {
|
|
44
|
+
clearTimeout(timer)
|
|
45
|
+
reject(new DOMException("Aborted", "AbortError"))
|
|
46
|
+
}
|
|
47
|
+
signal?.addEventListener("abort", onAbort, { once: true })
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function retryable(error: unknown, signal?: AbortSignal) {
|
|
52
|
+
if (signal?.aborted) return false
|
|
53
|
+
if (!(error instanceof Error)) return false
|
|
54
|
+
if (error.name === "AbortError" || error.name === "TimeoutError") return false
|
|
55
|
+
if (error instanceof TypeError) return true
|
|
56
|
+
return /network|fetch|econnreset|econnrefused|enotfound|timedout/i.test(error.message)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function checkServerHealth(
|
|
60
|
+
server: ServerConnection.HttpBase,
|
|
61
|
+
fetch: typeof globalThis.fetch,
|
|
62
|
+
opts?: CheckServerHealthOptions,
|
|
63
|
+
): Promise<ServerHealth> {
|
|
64
|
+
const timeout = opts?.signal ? undefined : timeoutSignal(opts?.timeoutMs ?? defaultTimeoutMs)
|
|
65
|
+
const signal = opts?.signal ?? timeout?.signal
|
|
66
|
+
const retryCount = opts?.retryCount ?? defaultRetryCount
|
|
67
|
+
const retryDelayMs = opts?.retryDelayMs ?? defaultRetryDelayMs
|
|
68
|
+
const next = (count: number, error: unknown) => {
|
|
69
|
+
if (count >= retryCount || !retryable(error, signal)) return Promise.resolve({ healthy: false } as const)
|
|
70
|
+
return wait(retryDelayMs * (count + 1), signal)
|
|
71
|
+
.then(() => attempt(count + 1))
|
|
72
|
+
.catch(() => ({ healthy: false }))
|
|
73
|
+
}
|
|
74
|
+
const attempt = (count: number): Promise<ServerHealth> =>
|
|
75
|
+
createSdkForServer({
|
|
76
|
+
server,
|
|
77
|
+
fetch,
|
|
78
|
+
signal,
|
|
79
|
+
})
|
|
80
|
+
.global.health()
|
|
81
|
+
.then((x) => (x.error ? next(count, x.error) : { healthy: x.data?.healthy === true, version: x.data?.version }))
|
|
82
|
+
.catch((error) => next(count, error))
|
|
83
|
+
return attempt(0).finally(() => timeout?.clear?.())
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function useCheckServerHealth() {
|
|
87
|
+
const platform = usePlatform()
|
|
88
|
+
const fetcher = platform.fetch ?? globalThis.fetch
|
|
89
|
+
|
|
90
|
+
return (http: ServerConnection.HttpBase) => checkServerHealth(http, fetcher)
|
|
91
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createOpencodeClient } from "@reign-labs/sdk/v2/client"
|
|
2
|
+
import type { ServerConnection } from "@/context/server"
|
|
3
|
+
|
|
4
|
+
export function createSdkForServer({
|
|
5
|
+
server,
|
|
6
|
+
...config
|
|
7
|
+
}: Omit<NonNullable<Parameters<typeof createOpencodeClient>[0]>, "baseUrl"> & {
|
|
8
|
+
server: ServerConnection.HttpBase
|
|
9
|
+
}) {
|
|
10
|
+
const auth = (() => {
|
|
11
|
+
if (!server.password) return
|
|
12
|
+
return {
|
|
13
|
+
Authorization: `Basic ${btoa(`${server.username ?? "opencode"}:${server.password}`)}`,
|
|
14
|
+
}
|
|
15
|
+
})()
|
|
16
|
+
|
|
17
|
+
return createOpencodeClient({
|
|
18
|
+
...config,
|
|
19
|
+
headers: { ...config.headers, ...auth },
|
|
20
|
+
baseUrl: server.url,
|
|
21
|
+
})
|
|
22
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { useDragDropContext } from "@thisbeyond/solid-dnd"
|
|
2
|
+
import type { Transformer } from "@thisbeyond/solid-dnd"
|
|
3
|
+
import { createRoot, onCleanup, type JSXElement } from "solid-js"
|
|
4
|
+
|
|
5
|
+
type DragEvent = { draggable?: { id?: unknown } }
|
|
6
|
+
|
|
7
|
+
const isDragEvent = (event: unknown): event is DragEvent => {
|
|
8
|
+
if (typeof event !== "object" || event === null) return false
|
|
9
|
+
return "draggable" in event
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const getDraggableId = (event: unknown): string | undefined => {
|
|
13
|
+
if (!isDragEvent(event)) return undefined
|
|
14
|
+
const draggable = event.draggable
|
|
15
|
+
if (!draggable) return undefined
|
|
16
|
+
return typeof draggable.id === "string" ? draggable.id : undefined
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const createTransformer = (id: string, axis: "x" | "y"): Transformer => ({
|
|
20
|
+
id,
|
|
21
|
+
order: 100,
|
|
22
|
+
callback: (transform) => (axis === "x" ? { ...transform, x: 0 } : { ...transform, y: 0 }),
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const createAxisConstraint = (axis: "x" | "y", transformerId: string) => (): JSXElement => {
|
|
26
|
+
const context = useDragDropContext()
|
|
27
|
+
if (!context) return null
|
|
28
|
+
const [, { onDragStart, onDragEnd, addTransformer, removeTransformer }] = context
|
|
29
|
+
const transformer = createTransformer(transformerId, axis)
|
|
30
|
+
const dispose = createRoot((dispose) => {
|
|
31
|
+
onDragStart((event) => {
|
|
32
|
+
const id = getDraggableId(event)
|
|
33
|
+
if (!id) return
|
|
34
|
+
addTransformer("draggables", id, transformer)
|
|
35
|
+
})
|
|
36
|
+
onDragEnd((event) => {
|
|
37
|
+
const id = getDraggableId(event)
|
|
38
|
+
if (!id) return
|
|
39
|
+
removeTransformer("draggables", id, transformer.id)
|
|
40
|
+
})
|
|
41
|
+
return dispose
|
|
42
|
+
})
|
|
43
|
+
onCleanup(dispose)
|
|
44
|
+
return null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const ConstrainDragXAxis = createAxisConstraint("x", "constrain-x-axis")
|
|
48
|
+
|
|
49
|
+
export const ConstrainDragYAxis = createAxisConstraint("y", "constrain-y-axis")
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import alert01 from "@reign-labs/ui/audio/alert-01.aac"
|
|
2
|
+
import alert02 from "@reign-labs/ui/audio/alert-02.aac"
|
|
3
|
+
import alert03 from "@reign-labs/ui/audio/alert-03.aac"
|
|
4
|
+
import alert04 from "@reign-labs/ui/audio/alert-04.aac"
|
|
5
|
+
import alert05 from "@reign-labs/ui/audio/alert-05.aac"
|
|
6
|
+
import alert06 from "@reign-labs/ui/audio/alert-06.aac"
|
|
7
|
+
import alert07 from "@reign-labs/ui/audio/alert-07.aac"
|
|
8
|
+
import alert08 from "@reign-labs/ui/audio/alert-08.aac"
|
|
9
|
+
import alert09 from "@reign-labs/ui/audio/alert-09.aac"
|
|
10
|
+
import alert10 from "@reign-labs/ui/audio/alert-10.aac"
|
|
11
|
+
import bipbop01 from "@reign-labs/ui/audio/bip-bop-01.aac"
|
|
12
|
+
import bipbop02 from "@reign-labs/ui/audio/bip-bop-02.aac"
|
|
13
|
+
import bipbop03 from "@reign-labs/ui/audio/bip-bop-03.aac"
|
|
14
|
+
import bipbop04 from "@reign-labs/ui/audio/bip-bop-04.aac"
|
|
15
|
+
import bipbop05 from "@reign-labs/ui/audio/bip-bop-05.aac"
|
|
16
|
+
import bipbop06 from "@reign-labs/ui/audio/bip-bop-06.aac"
|
|
17
|
+
import bipbop07 from "@reign-labs/ui/audio/bip-bop-07.aac"
|
|
18
|
+
import bipbop08 from "@reign-labs/ui/audio/bip-bop-08.aac"
|
|
19
|
+
import bipbop09 from "@reign-labs/ui/audio/bip-bop-09.aac"
|
|
20
|
+
import bipbop10 from "@reign-labs/ui/audio/bip-bop-10.aac"
|
|
21
|
+
import nope01 from "@reign-labs/ui/audio/nope-01.aac"
|
|
22
|
+
import nope02 from "@reign-labs/ui/audio/nope-02.aac"
|
|
23
|
+
import nope03 from "@reign-labs/ui/audio/nope-03.aac"
|
|
24
|
+
import nope04 from "@reign-labs/ui/audio/nope-04.aac"
|
|
25
|
+
import nope05 from "@reign-labs/ui/audio/nope-05.aac"
|
|
26
|
+
import nope06 from "@reign-labs/ui/audio/nope-06.aac"
|
|
27
|
+
import nope07 from "@reign-labs/ui/audio/nope-07.aac"
|
|
28
|
+
import nope08 from "@reign-labs/ui/audio/nope-08.aac"
|
|
29
|
+
import nope09 from "@reign-labs/ui/audio/nope-09.aac"
|
|
30
|
+
import nope10 from "@reign-labs/ui/audio/nope-10.aac"
|
|
31
|
+
import nope11 from "@reign-labs/ui/audio/nope-11.aac"
|
|
32
|
+
import nope12 from "@reign-labs/ui/audio/nope-12.aac"
|
|
33
|
+
import staplebops01 from "@reign-labs/ui/audio/staplebops-01.aac"
|
|
34
|
+
import staplebops02 from "@reign-labs/ui/audio/staplebops-02.aac"
|
|
35
|
+
import staplebops03 from "@reign-labs/ui/audio/staplebops-03.aac"
|
|
36
|
+
import staplebops04 from "@reign-labs/ui/audio/staplebops-04.aac"
|
|
37
|
+
import staplebops05 from "@reign-labs/ui/audio/staplebops-05.aac"
|
|
38
|
+
import staplebops06 from "@reign-labs/ui/audio/staplebops-06.aac"
|
|
39
|
+
import staplebops07 from "@reign-labs/ui/audio/staplebops-07.aac"
|
|
40
|
+
import yup01 from "@reign-labs/ui/audio/yup-01.aac"
|
|
41
|
+
import yup02 from "@reign-labs/ui/audio/yup-02.aac"
|
|
42
|
+
import yup03 from "@reign-labs/ui/audio/yup-03.aac"
|
|
43
|
+
import yup04 from "@reign-labs/ui/audio/yup-04.aac"
|
|
44
|
+
import yup05 from "@reign-labs/ui/audio/yup-05.aac"
|
|
45
|
+
import yup06 from "@reign-labs/ui/audio/yup-06.aac"
|
|
46
|
+
|
|
47
|
+
export const SOUND_OPTIONS = [
|
|
48
|
+
{ id: "alert-01", label: "sound.option.alert01", src: alert01 },
|
|
49
|
+
{ id: "alert-02", label: "sound.option.alert02", src: alert02 },
|
|
50
|
+
{ id: "alert-03", label: "sound.option.alert03", src: alert03 },
|
|
51
|
+
{ id: "alert-04", label: "sound.option.alert04", src: alert04 },
|
|
52
|
+
{ id: "alert-05", label: "sound.option.alert05", src: alert05 },
|
|
53
|
+
{ id: "alert-06", label: "sound.option.alert06", src: alert06 },
|
|
54
|
+
{ id: "alert-07", label: "sound.option.alert07", src: alert07 },
|
|
55
|
+
{ id: "alert-08", label: "sound.option.alert08", src: alert08 },
|
|
56
|
+
{ id: "alert-09", label: "sound.option.alert09", src: alert09 },
|
|
57
|
+
{ id: "alert-10", label: "sound.option.alert10", src: alert10 },
|
|
58
|
+
{ id: "bip-bop-01", label: "sound.option.bipbop01", src: bipbop01 },
|
|
59
|
+
{ id: "bip-bop-02", label: "sound.option.bipbop02", src: bipbop02 },
|
|
60
|
+
{ id: "bip-bop-03", label: "sound.option.bipbop03", src: bipbop03 },
|
|
61
|
+
{ id: "bip-bop-04", label: "sound.option.bipbop04", src: bipbop04 },
|
|
62
|
+
{ id: "bip-bop-05", label: "sound.option.bipbop05", src: bipbop05 },
|
|
63
|
+
{ id: "bip-bop-06", label: "sound.option.bipbop06", src: bipbop06 },
|
|
64
|
+
{ id: "bip-bop-07", label: "sound.option.bipbop07", src: bipbop07 },
|
|
65
|
+
{ id: "bip-bop-08", label: "sound.option.bipbop08", src: bipbop08 },
|
|
66
|
+
{ id: "bip-bop-09", label: "sound.option.bipbop09", src: bipbop09 },
|
|
67
|
+
{ id: "bip-bop-10", label: "sound.option.bipbop10", src: bipbop10 },
|
|
68
|
+
{ id: "staplebops-01", label: "sound.option.staplebops01", src: staplebops01 },
|
|
69
|
+
{ id: "staplebops-02", label: "sound.option.staplebops02", src: staplebops02 },
|
|
70
|
+
{ id: "staplebops-03", label: "sound.option.staplebops03", src: staplebops03 },
|
|
71
|
+
{ id: "staplebops-04", label: "sound.option.staplebops04", src: staplebops04 },
|
|
72
|
+
{ id: "staplebops-05", label: "sound.option.staplebops05", src: staplebops05 },
|
|
73
|
+
{ id: "staplebops-06", label: "sound.option.staplebops06", src: staplebops06 },
|
|
74
|
+
{ id: "staplebops-07", label: "sound.option.staplebops07", src: staplebops07 },
|
|
75
|
+
{ id: "nope-01", label: "sound.option.nope01", src: nope01 },
|
|
76
|
+
{ id: "nope-02", label: "sound.option.nope02", src: nope02 },
|
|
77
|
+
{ id: "nope-03", label: "sound.option.nope03", src: nope03 },
|
|
78
|
+
{ id: "nope-04", label: "sound.option.nope04", src: nope04 },
|
|
79
|
+
{ id: "nope-05", label: "sound.option.nope05", src: nope05 },
|
|
80
|
+
{ id: "nope-06", label: "sound.option.nope06", src: nope06 },
|
|
81
|
+
{ id: "nope-07", label: "sound.option.nope07", src: nope07 },
|
|
82
|
+
{ id: "nope-08", label: "sound.option.nope08", src: nope08 },
|
|
83
|
+
{ id: "nope-09", label: "sound.option.nope09", src: nope09 },
|
|
84
|
+
{ id: "nope-10", label: "sound.option.nope10", src: nope10 },
|
|
85
|
+
{ id: "nope-11", label: "sound.option.nope11", src: nope11 },
|
|
86
|
+
{ id: "nope-12", label: "sound.option.nope12", src: nope12 },
|
|
87
|
+
{ id: "yup-01", label: "sound.option.yup01", src: yup01 },
|
|
88
|
+
{ id: "yup-02", label: "sound.option.yup02", src: yup02 },
|
|
89
|
+
{ id: "yup-03", label: "sound.option.yup03", src: yup03 },
|
|
90
|
+
{ id: "yup-04", label: "sound.option.yup04", src: yup04 },
|
|
91
|
+
{ id: "yup-05", label: "sound.option.yup05", src: yup05 },
|
|
92
|
+
{ id: "yup-06", label: "sound.option.yup06", src: yup06 },
|
|
93
|
+
] as const
|
|
94
|
+
|
|
95
|
+
export type SoundOption = (typeof SOUND_OPTIONS)[number]
|
|
96
|
+
export type SoundID = SoundOption["id"]
|
|
97
|
+
|
|
98
|
+
const soundById = Object.fromEntries(SOUND_OPTIONS.map((s) => [s.id, s.src])) as Record<SoundID, string>
|
|
99
|
+
|
|
100
|
+
export function soundSrc(id: string | undefined) {
|
|
101
|
+
if (!id) return
|
|
102
|
+
if (!(id in soundById)) return
|
|
103
|
+
return soundById[id as SoundID]
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function playSound(src: string | undefined) {
|
|
107
|
+
if (typeof Audio === "undefined") return
|
|
108
|
+
if (!src) return
|
|
109
|
+
const audio = new Audio(src)
|
|
110
|
+
audio.play().catch(() => undefined)
|
|
111
|
+
|
|
112
|
+
// Return a cleanup function to pause the sound.
|
|
113
|
+
return () => {
|
|
114
|
+
audio.pause()
|
|
115
|
+
audio.currentTime = 0
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import { terminalWriter } from "./terminal-writer"
|
|
3
|
+
|
|
4
|
+
describe("terminalWriter", () => {
|
|
5
|
+
test("buffers and flushes once per schedule", () => {
|
|
6
|
+
const calls: string[] = []
|
|
7
|
+
const scheduled: VoidFunction[] = []
|
|
8
|
+
const writer = terminalWriter(
|
|
9
|
+
(data, done) => {
|
|
10
|
+
calls.push(data)
|
|
11
|
+
done?.()
|
|
12
|
+
},
|
|
13
|
+
(flush) => scheduled.push(flush),
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
writer.push("a")
|
|
17
|
+
writer.push("b")
|
|
18
|
+
writer.push("c")
|
|
19
|
+
|
|
20
|
+
expect(calls).toEqual([])
|
|
21
|
+
expect(scheduled).toHaveLength(1)
|
|
22
|
+
|
|
23
|
+
scheduled[0]?.()
|
|
24
|
+
expect(calls).toEqual(["abc"])
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test("flush is a no-op when empty", () => {
|
|
28
|
+
const calls: string[] = []
|
|
29
|
+
const writer = terminalWriter(
|
|
30
|
+
(data, done) => {
|
|
31
|
+
calls.push(data)
|
|
32
|
+
done?.()
|
|
33
|
+
},
|
|
34
|
+
(flush) => flush(),
|
|
35
|
+
)
|
|
36
|
+
writer.flush()
|
|
37
|
+
expect(calls).toEqual([])
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test("flush waits for pending write completion", () => {
|
|
41
|
+
const calls: string[] = []
|
|
42
|
+
let done: VoidFunction | undefined
|
|
43
|
+
const writer = terminalWriter(
|
|
44
|
+
(data, finish) => {
|
|
45
|
+
calls.push(data)
|
|
46
|
+
done = finish
|
|
47
|
+
},
|
|
48
|
+
(flush) => flush(),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
writer.push("a")
|
|
52
|
+
|
|
53
|
+
let settled = false
|
|
54
|
+
writer.flush(() => {
|
|
55
|
+
settled = true
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
expect(calls).toEqual(["a"])
|
|
59
|
+
expect(settled).toBe(false)
|
|
60
|
+
|
|
61
|
+
done?.()
|
|
62
|
+
expect(settled).toBe(true)
|
|
63
|
+
})
|
|
64
|
+
})
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export function terminalWriter(
|
|
2
|
+
write: (data: string, done?: VoidFunction) => void,
|
|
3
|
+
schedule: (flush: VoidFunction) => void = queueMicrotask,
|
|
4
|
+
) {
|
|
5
|
+
let chunks: string[] | undefined
|
|
6
|
+
let waits: VoidFunction[] | undefined
|
|
7
|
+
let scheduled = false
|
|
8
|
+
let writing = false
|
|
9
|
+
|
|
10
|
+
const settle = () => {
|
|
11
|
+
if (scheduled || writing || chunks?.length) return
|
|
12
|
+
const list = waits
|
|
13
|
+
if (!list?.length) return
|
|
14
|
+
waits = undefined
|
|
15
|
+
for (const fn of list) {
|
|
16
|
+
fn()
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const run = () => {
|
|
21
|
+
if (writing) return
|
|
22
|
+
scheduled = false
|
|
23
|
+
const items = chunks
|
|
24
|
+
if (!items?.length) {
|
|
25
|
+
settle()
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
chunks = undefined
|
|
29
|
+
writing = true
|
|
30
|
+
write(items.join(""), () => {
|
|
31
|
+
writing = false
|
|
32
|
+
if (chunks?.length) {
|
|
33
|
+
if (scheduled) return
|
|
34
|
+
scheduled = true
|
|
35
|
+
schedule(run)
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
settle()
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const push = (data: string) => {
|
|
43
|
+
if (!data) return
|
|
44
|
+
if (chunks) chunks.push(data)
|
|
45
|
+
else chunks = [data]
|
|
46
|
+
|
|
47
|
+
if (scheduled || writing) return
|
|
48
|
+
scheduled = true
|
|
49
|
+
schedule(run)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const flush = (done?: VoidFunction) => {
|
|
53
|
+
if (!scheduled && !writing && !chunks?.length) {
|
|
54
|
+
done?.()
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
if (done) {
|
|
58
|
+
if (waits) waits.push(done)
|
|
59
|
+
else waits = [done]
|
|
60
|
+
}
|
|
61
|
+
run()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return { push, flush }
|
|
65
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
type TimeKey =
|
|
2
|
+
| "common.time.justNow"
|
|
3
|
+
| "common.time.minutesAgo.short"
|
|
4
|
+
| "common.time.hoursAgo.short"
|
|
5
|
+
| "common.time.daysAgo.short"
|
|
6
|
+
|
|
7
|
+
type Translate = (key: TimeKey, params?: Record<string, string | number>) => string
|
|
8
|
+
|
|
9
|
+
export function getRelativeTime(dateString: string, t: Translate): string {
|
|
10
|
+
const date = new Date(dateString)
|
|
11
|
+
const now = new Date()
|
|
12
|
+
const diffMs = now.getTime() - date.getTime()
|
|
13
|
+
const diffSeconds = Math.floor(diffMs / 1000)
|
|
14
|
+
const diffMinutes = Math.floor(diffSeconds / 60)
|
|
15
|
+
const diffHours = Math.floor(diffMinutes / 60)
|
|
16
|
+
const diffDays = Math.floor(diffHours / 24)
|
|
17
|
+
|
|
18
|
+
if (diffSeconds < 60) return t("common.time.justNow")
|
|
19
|
+
if (diffMinutes < 60) return t("common.time.minutesAgo.short", { count: diffMinutes })
|
|
20
|
+
if (diffHours < 24) return t("common.time.hoursAgo.short", { count: diffHours })
|
|
21
|
+
return t("common.time.daysAgo.short", { count: diffDays })
|
|
22
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test"
|
|
2
|
+
import { uuid } from "./uuid"
|
|
3
|
+
|
|
4
|
+
const cryptoDescriptor = Object.getOwnPropertyDescriptor(globalThis, "crypto")
|
|
5
|
+
const secureDescriptor = Object.getOwnPropertyDescriptor(globalThis, "isSecureContext")
|
|
6
|
+
const randomDescriptor = Object.getOwnPropertyDescriptor(Math, "random")
|
|
7
|
+
|
|
8
|
+
const setCrypto = (value: Partial<Crypto>) => {
|
|
9
|
+
Object.defineProperty(globalThis, "crypto", {
|
|
10
|
+
configurable: true,
|
|
11
|
+
value: value as Crypto,
|
|
12
|
+
})
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const setSecure = (value: boolean) => {
|
|
16
|
+
Object.defineProperty(globalThis, "isSecureContext", {
|
|
17
|
+
configurable: true,
|
|
18
|
+
value,
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const setRandom = (value: () => number) => {
|
|
23
|
+
Object.defineProperty(Math, "random", {
|
|
24
|
+
configurable: true,
|
|
25
|
+
value,
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
if (cryptoDescriptor) {
|
|
31
|
+
Object.defineProperty(globalThis, "crypto", cryptoDescriptor)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (secureDescriptor) {
|
|
35
|
+
Object.defineProperty(globalThis, "isSecureContext", secureDescriptor)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!secureDescriptor) {
|
|
39
|
+
delete (globalThis as { isSecureContext?: boolean }).isSecureContext
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (randomDescriptor) {
|
|
43
|
+
Object.defineProperty(Math, "random", randomDescriptor)
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
describe("uuid", () => {
|
|
48
|
+
test("uses randomUUID in secure contexts", () => {
|
|
49
|
+
setCrypto({ randomUUID: () => "00000000-0000-0000-0000-000000000000" })
|
|
50
|
+
setSecure(true)
|
|
51
|
+
expect(uuid()).toBe("00000000-0000-0000-0000-000000000000")
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test("falls back in insecure contexts", () => {
|
|
55
|
+
setCrypto({ randomUUID: () => "00000000-0000-0000-0000-000000000000" })
|
|
56
|
+
setSecure(false)
|
|
57
|
+
setRandom(() => 0.5)
|
|
58
|
+
expect(uuid()).toBe("8")
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test("falls back when randomUUID throws", () => {
|
|
62
|
+
setCrypto({
|
|
63
|
+
randomUUID: () => {
|
|
64
|
+
throw new DOMException("Failed", "OperationError")
|
|
65
|
+
},
|
|
66
|
+
})
|
|
67
|
+
setSecure(true)
|
|
68
|
+
setRandom(() => 0.5)
|
|
69
|
+
expect(uuid()).toBe("8")
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
test("falls back when randomUUID is unavailable", () => {
|
|
73
|
+
setCrypto({})
|
|
74
|
+
setSecure(true)
|
|
75
|
+
setRandom(() => 0.5)
|
|
76
|
+
expect(uuid()).toBe("8")
|
|
77
|
+
})
|
|
78
|
+
})
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const fallback = () => Math.random().toString(16).slice(2)
|
|
2
|
+
|
|
3
|
+
export function uuid() {
|
|
4
|
+
const c = globalThis.crypto
|
|
5
|
+
if (!c || typeof c.randomUUID !== "function") return fallback()
|
|
6
|
+
if (typeof globalThis.isSecureContext === "boolean" && !globalThis.isSecureContext) return fallback()
|
|
7
|
+
try {
|
|
8
|
+
return c.randomUUID()
|
|
9
|
+
} catch {
|
|
10
|
+
return fallback()
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import { Worktree } from "./worktree"
|
|
3
|
+
|
|
4
|
+
const dir = (name: string) => `/tmp/opencode-worktree-${name}-${crypto.randomUUID()}`
|
|
5
|
+
|
|
6
|
+
describe("Worktree", () => {
|
|
7
|
+
test("normalizes trailing slashes", () => {
|
|
8
|
+
const key = dir("normalize")
|
|
9
|
+
Worktree.ready(`${key}/`)
|
|
10
|
+
|
|
11
|
+
expect(Worktree.get(key)).toEqual({ status: "ready" })
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
test("pending does not overwrite a terminal state", () => {
|
|
15
|
+
const key = dir("pending")
|
|
16
|
+
Worktree.failed(key, "boom")
|
|
17
|
+
Worktree.pending(key)
|
|
18
|
+
|
|
19
|
+
expect(Worktree.get(key)).toEqual({ status: "failed", message: "boom" })
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test("wait resolves shared pending waiter when ready", async () => {
|
|
23
|
+
const key = dir("wait-ready")
|
|
24
|
+
Worktree.pending(key)
|
|
25
|
+
|
|
26
|
+
const a = Worktree.wait(key)
|
|
27
|
+
const b = Worktree.wait(`${key}/`)
|
|
28
|
+
|
|
29
|
+
expect(a).toBe(b)
|
|
30
|
+
|
|
31
|
+
Worktree.ready(key)
|
|
32
|
+
|
|
33
|
+
expect(await a).toEqual({ status: "ready" })
|
|
34
|
+
expect(await b).toEqual({ status: "ready" })
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test("wait resolves with failure message", async () => {
|
|
38
|
+
const key = dir("wait-failed")
|
|
39
|
+
const waiting = Worktree.wait(key)
|
|
40
|
+
|
|
41
|
+
Worktree.failed(key, "permission denied")
|
|
42
|
+
|
|
43
|
+
expect(await waiting).toEqual({ status: "failed", message: "permission denied" })
|
|
44
|
+
expect(await Worktree.wait(key)).toEqual({ status: "failed", message: "permission denied" })
|
|
45
|
+
})
|
|
46
|
+
})
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
const normalize = (directory: string) => directory.replace(/[\\/]+$/, "")
|
|
2
|
+
|
|
3
|
+
type State =
|
|
4
|
+
| {
|
|
5
|
+
status: "pending"
|
|
6
|
+
}
|
|
7
|
+
| {
|
|
8
|
+
status: "ready"
|
|
9
|
+
}
|
|
10
|
+
| {
|
|
11
|
+
status: "failed"
|
|
12
|
+
message: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const state = new Map<string, State>()
|
|
16
|
+
const waiters = new Map<
|
|
17
|
+
string,
|
|
18
|
+
{
|
|
19
|
+
promise: Promise<State>
|
|
20
|
+
resolve: (state: State) => void
|
|
21
|
+
}
|
|
22
|
+
>()
|
|
23
|
+
|
|
24
|
+
function deferred() {
|
|
25
|
+
const box = { resolve: (_: State) => {} }
|
|
26
|
+
const promise = new Promise<State>((resolve) => {
|
|
27
|
+
box.resolve = resolve
|
|
28
|
+
})
|
|
29
|
+
return { promise, resolve: box.resolve }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const Worktree = {
|
|
33
|
+
get(directory: string) {
|
|
34
|
+
return state.get(normalize(directory))
|
|
35
|
+
},
|
|
36
|
+
pending(directory: string) {
|
|
37
|
+
const key = normalize(directory)
|
|
38
|
+
const current = state.get(key)
|
|
39
|
+
if (current && current.status !== "pending") return
|
|
40
|
+
state.set(key, { status: "pending" })
|
|
41
|
+
},
|
|
42
|
+
ready(directory: string) {
|
|
43
|
+
const key = normalize(directory)
|
|
44
|
+
const next = { status: "ready" } as const
|
|
45
|
+
state.set(key, next)
|
|
46
|
+
const waiter = waiters.get(key)
|
|
47
|
+
if (!waiter) return
|
|
48
|
+
waiters.delete(key)
|
|
49
|
+
waiter.resolve(next)
|
|
50
|
+
},
|
|
51
|
+
failed(directory: string, message: string) {
|
|
52
|
+
const key = normalize(directory)
|
|
53
|
+
const next = { status: "failed", message } as const
|
|
54
|
+
state.set(key, next)
|
|
55
|
+
const waiter = waiters.get(key)
|
|
56
|
+
if (!waiter) return
|
|
57
|
+
waiters.delete(key)
|
|
58
|
+
waiter.resolve(next)
|
|
59
|
+
},
|
|
60
|
+
wait(directory: string) {
|
|
61
|
+
const key = normalize(directory)
|
|
62
|
+
const current = state.get(key)
|
|
63
|
+
if (current && current.status !== "pending") return Promise.resolve(current)
|
|
64
|
+
|
|
65
|
+
const existing = waiters.get(key)
|
|
66
|
+
if (existing) return existing.promise
|
|
67
|
+
|
|
68
|
+
const waiter = deferred()
|
|
69
|
+
|
|
70
|
+
waiters.set(key, waiter)
|
|
71
|
+
return waiter.promise
|
|
72
|
+
},
|
|
73
|
+
}
|
package/sst-env.d.ts
ADDED
package/tsconfig.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/tsconfig",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"composite": true,
|
|
5
|
+
"target": "ESNext",
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
"moduleResolution": "bundler",
|
|
9
|
+
"allowSyntheticDefaultImports": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"jsx": "preserve",
|
|
12
|
+
"jsxImportSource": "solid-js",
|
|
13
|
+
"allowJs": true,
|
|
14
|
+
"resolveJsonModule": true,
|
|
15
|
+
"strict": true,
|
|
16
|
+
"noEmit": false,
|
|
17
|
+
"emitDeclarationOnly": true,
|
|
18
|
+
"outDir": "node_modules/.ts-dist",
|
|
19
|
+
"isolatedModules": true,
|
|
20
|
+
"paths": {
|
|
21
|
+
"@/*": ["./src/*"]
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"include": ["src", "package.json"],
|
|
25
|
+
"exclude": ["dist", "ts-dist"]
|
|
26
|
+
}
|