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,118 @@
|
|
|
1
|
+
import { test, expect } from "../fixtures"
|
|
2
|
+
import {
|
|
3
|
+
defocus,
|
|
4
|
+
cleanupSession,
|
|
5
|
+
cleanupTestProject,
|
|
6
|
+
closeSidebar,
|
|
7
|
+
createTestProject,
|
|
8
|
+
hoverSessionItem,
|
|
9
|
+
openSidebar,
|
|
10
|
+
waitSession,
|
|
11
|
+
} from "../actions"
|
|
12
|
+
import { projectSwitchSelector } from "../selectors"
|
|
13
|
+
import { dirSlug } from "../utils"
|
|
14
|
+
|
|
15
|
+
test("collapsed sidebar popover stays open when archiving a session", async ({ page, slug, sdk, gotoSession }) => {
|
|
16
|
+
const stamp = Date.now()
|
|
17
|
+
|
|
18
|
+
const one = await sdk.session.create({ title: `e2e sidebar popover archive 1 ${stamp}` }).then((r) => r.data)
|
|
19
|
+
const two = await sdk.session.create({ title: `e2e sidebar popover archive 2 ${stamp}` }).then((r) => r.data)
|
|
20
|
+
|
|
21
|
+
if (!one?.id) throw new Error("Session create did not return an id")
|
|
22
|
+
if (!two?.id) throw new Error("Session create did not return an id")
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
await gotoSession(one.id)
|
|
26
|
+
await closeSidebar(page)
|
|
27
|
+
|
|
28
|
+
const oneItem = page.locator(`[data-session-id="${one.id}"]`).last()
|
|
29
|
+
const twoItem = page.locator(`[data-session-id="${two.id}"]`).last()
|
|
30
|
+
|
|
31
|
+
const project = page.locator(projectSwitchSelector(slug)).first()
|
|
32
|
+
await expect(project).toBeVisible()
|
|
33
|
+
await project.hover()
|
|
34
|
+
|
|
35
|
+
await expect(oneItem).toBeVisible()
|
|
36
|
+
await expect(twoItem).toBeVisible()
|
|
37
|
+
|
|
38
|
+
const item = await hoverSessionItem(page, one.id)
|
|
39
|
+
await item
|
|
40
|
+
.getByRole("button", { name: /archive/i })
|
|
41
|
+
.first()
|
|
42
|
+
.click()
|
|
43
|
+
|
|
44
|
+
await expect(twoItem).toBeVisible()
|
|
45
|
+
} finally {
|
|
46
|
+
await cleanupSession({ sdk, sessionID: one.id })
|
|
47
|
+
await cleanupSession({ sdk, sessionID: two.id })
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test("open sidebar project popover stays closed after clicking avatar", async ({ page, withProject }) => {
|
|
52
|
+
await page.setViewportSize({ width: 1400, height: 800 })
|
|
53
|
+
|
|
54
|
+
const other = await createTestProject()
|
|
55
|
+
const slug = dirSlug(other)
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
await withProject(
|
|
59
|
+
async () => {
|
|
60
|
+
await openSidebar(page)
|
|
61
|
+
|
|
62
|
+
const project = page.locator(projectSwitchSelector(slug)).first()
|
|
63
|
+
const card = page.locator('[data-component="hover-card-content"]')
|
|
64
|
+
|
|
65
|
+
await expect(project).toBeVisible()
|
|
66
|
+
await project.hover()
|
|
67
|
+
await expect(card.getByText(/recent sessions/i)).toBeVisible()
|
|
68
|
+
|
|
69
|
+
await page.mouse.down()
|
|
70
|
+
await expect(card).toHaveCount(0)
|
|
71
|
+
await page.mouse.up()
|
|
72
|
+
|
|
73
|
+
await waitSession(page, { directory: other })
|
|
74
|
+
await expect(card).toHaveCount(0)
|
|
75
|
+
},
|
|
76
|
+
{ extra: [other] },
|
|
77
|
+
)
|
|
78
|
+
} finally {
|
|
79
|
+
await cleanupTestProject(other)
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test("open sidebar project switch activates on first tabbed enter", async ({ page, withProject }) => {
|
|
84
|
+
await page.setViewportSize({ width: 1400, height: 800 })
|
|
85
|
+
|
|
86
|
+
const other = await createTestProject()
|
|
87
|
+
const slug = dirSlug(other)
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
await withProject(
|
|
91
|
+
async () => {
|
|
92
|
+
await openSidebar(page)
|
|
93
|
+
await defocus(page)
|
|
94
|
+
|
|
95
|
+
const project = page.locator(projectSwitchSelector(slug)).first()
|
|
96
|
+
|
|
97
|
+
await expect(project).toBeVisible()
|
|
98
|
+
|
|
99
|
+
let hit = false
|
|
100
|
+
for (let i = 0; i < 20; i++) {
|
|
101
|
+
hit = await project.evaluate((el) => {
|
|
102
|
+
return el.matches(":focus") || !!el.parentElement?.matches(":focus")
|
|
103
|
+
})
|
|
104
|
+
if (hit) break
|
|
105
|
+
await page.keyboard.press("Tab")
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
expect(hit).toBe(true)
|
|
109
|
+
|
|
110
|
+
await page.keyboard.press("Enter")
|
|
111
|
+
await waitSession(page, { directory: other })
|
|
112
|
+
},
|
|
113
|
+
{ extra: [other] },
|
|
114
|
+
)
|
|
115
|
+
} finally {
|
|
116
|
+
await cleanupTestProject(other)
|
|
117
|
+
}
|
|
118
|
+
})
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { test, expect } from "../fixtures"
|
|
2
|
+
import { cleanupSession, openSidebar, withSession } from "../actions"
|
|
3
|
+
import { promptSelector } from "../selectors"
|
|
4
|
+
|
|
5
|
+
test("sidebar session links navigate to the selected session", async ({ page, slug, sdk, gotoSession }) => {
|
|
6
|
+
const stamp = Date.now()
|
|
7
|
+
|
|
8
|
+
const one = await sdk.session.create({ title: `e2e sidebar nav 1 ${stamp}` }).then((r) => r.data)
|
|
9
|
+
const two = await sdk.session.create({ title: `e2e sidebar nav 2 ${stamp}` }).then((r) => r.data)
|
|
10
|
+
|
|
11
|
+
if (!one?.id) throw new Error("Session create did not return an id")
|
|
12
|
+
if (!two?.id) throw new Error("Session create did not return an id")
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
await gotoSession(one.id)
|
|
16
|
+
|
|
17
|
+
await openSidebar(page)
|
|
18
|
+
|
|
19
|
+
const target = page.locator(`[data-session-id="${two.id}"] a`).first()
|
|
20
|
+
await expect(target).toBeVisible()
|
|
21
|
+
await target.click()
|
|
22
|
+
|
|
23
|
+
await expect(page).toHaveURL(new RegExp(`/${slug}/session/${two.id}(?:\\?|#|$)`))
|
|
24
|
+
await expect(page.locator(promptSelector)).toBeVisible()
|
|
25
|
+
await expect(page.locator(`[data-session-id="${two.id}"] a`).first()).toHaveClass(/\bactive\b/)
|
|
26
|
+
} finally {
|
|
27
|
+
await cleanupSession({ sdk, sessionID: one.id })
|
|
28
|
+
await cleanupSession({ sdk, sessionID: two.id })
|
|
29
|
+
}
|
|
30
|
+
})
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { test, expect } from "../fixtures"
|
|
2
|
+
import { openSidebar, toggleSidebar, withSession } from "../actions"
|
|
3
|
+
|
|
4
|
+
test("sidebar can be collapsed and expanded", async ({ page, gotoSession }) => {
|
|
5
|
+
await gotoSession()
|
|
6
|
+
|
|
7
|
+
await openSidebar(page)
|
|
8
|
+
const button = page.getByRole("button", { name: /toggle sidebar/i }).first()
|
|
9
|
+
await expect(button).toHaveAttribute("aria-expanded", "true")
|
|
10
|
+
|
|
11
|
+
await toggleSidebar(page)
|
|
12
|
+
await expect(button).toHaveAttribute("aria-expanded", "false")
|
|
13
|
+
|
|
14
|
+
await toggleSidebar(page)
|
|
15
|
+
await expect(button).toHaveAttribute("aria-expanded", "true")
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test("sidebar collapsed state persists across navigation and reload", async ({ page, sdk, gotoSession }) => {
|
|
19
|
+
await withSession(sdk, "sidebar persist session 1", async (session1) => {
|
|
20
|
+
await withSession(sdk, "sidebar persist session 2", async (session2) => {
|
|
21
|
+
await gotoSession(session1.id)
|
|
22
|
+
|
|
23
|
+
await openSidebar(page)
|
|
24
|
+
const button = page.getByRole("button", { name: /toggle sidebar/i }).first()
|
|
25
|
+
await toggleSidebar(page)
|
|
26
|
+
await expect(button).toHaveAttribute("aria-expanded", "false")
|
|
27
|
+
|
|
28
|
+
await gotoSession(session2.id)
|
|
29
|
+
await expect(button).toHaveAttribute("aria-expanded", "false")
|
|
30
|
+
|
|
31
|
+
await page.reload()
|
|
32
|
+
await expect(button).toHaveAttribute("aria-expanded", "false")
|
|
33
|
+
|
|
34
|
+
const opened = await page.evaluate(
|
|
35
|
+
() => JSON.parse(localStorage.getItem("opencode.global.dat:layout") ?? "{}").sidebar?.opened,
|
|
36
|
+
)
|
|
37
|
+
await expect(opened).toBe(false)
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
})
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { test, expect } from "../fixtures"
|
|
2
|
+
import { openStatusPopover } from "../actions"
|
|
3
|
+
|
|
4
|
+
test("status popover opens and shows tabs", async ({ page, gotoSession }) => {
|
|
5
|
+
await gotoSession()
|
|
6
|
+
|
|
7
|
+
const { popoverBody } = await openStatusPopover(page)
|
|
8
|
+
|
|
9
|
+
await expect(popoverBody.getByRole("tab", { name: /servers/i })).toBeVisible()
|
|
10
|
+
await expect(popoverBody.getByRole("tab", { name: /mcp/i })).toBeVisible()
|
|
11
|
+
await expect(popoverBody.getByRole("tab", { name: /lsp/i })).toBeVisible()
|
|
12
|
+
await expect(popoverBody.getByRole("tab", { name: /plugins/i })).toBeVisible()
|
|
13
|
+
|
|
14
|
+
await page.keyboard.press("Escape")
|
|
15
|
+
await expect(popoverBody).toHaveCount(0)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test("status popover servers tab shows current server", async ({ page, gotoSession }) => {
|
|
19
|
+
await gotoSession()
|
|
20
|
+
|
|
21
|
+
const { popoverBody } = await openStatusPopover(page)
|
|
22
|
+
|
|
23
|
+
const serversTab = popoverBody.getByRole("tab", { name: /servers/i })
|
|
24
|
+
await expect(serversTab).toHaveAttribute("aria-selected", "true")
|
|
25
|
+
|
|
26
|
+
const serverList = popoverBody.locator('[role="tabpanel"]').first()
|
|
27
|
+
await expect(serverList.locator("button").first()).toBeVisible()
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test("status popover can switch to mcp tab", async ({ page, gotoSession }) => {
|
|
31
|
+
await gotoSession()
|
|
32
|
+
|
|
33
|
+
const { popoverBody } = await openStatusPopover(page)
|
|
34
|
+
|
|
35
|
+
const mcpTab = popoverBody.getByRole("tab", { name: /mcp/i })
|
|
36
|
+
await mcpTab.click()
|
|
37
|
+
|
|
38
|
+
const ariaSelected = await mcpTab.getAttribute("aria-selected")
|
|
39
|
+
expect(ariaSelected).toBe("true")
|
|
40
|
+
|
|
41
|
+
const mcpContent = popoverBody.locator('[role="tabpanel"]:visible').first()
|
|
42
|
+
await expect(mcpContent).toBeVisible()
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test("status popover can switch to lsp tab", async ({ page, gotoSession }) => {
|
|
46
|
+
await gotoSession()
|
|
47
|
+
|
|
48
|
+
const { popoverBody } = await openStatusPopover(page)
|
|
49
|
+
|
|
50
|
+
const lspTab = popoverBody.getByRole("tab", { name: /lsp/i })
|
|
51
|
+
await lspTab.click()
|
|
52
|
+
|
|
53
|
+
const ariaSelected = await lspTab.getAttribute("aria-selected")
|
|
54
|
+
expect(ariaSelected).toBe("true")
|
|
55
|
+
|
|
56
|
+
const lspContent = popoverBody.locator('[role="tabpanel"]:visible').first()
|
|
57
|
+
await expect(lspContent).toBeVisible()
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test("status popover can switch to plugins tab", async ({ page, gotoSession }) => {
|
|
61
|
+
await gotoSession()
|
|
62
|
+
|
|
63
|
+
const { popoverBody } = await openStatusPopover(page)
|
|
64
|
+
|
|
65
|
+
const pluginsTab = popoverBody.getByRole("tab", { name: /plugins/i })
|
|
66
|
+
await pluginsTab.click()
|
|
67
|
+
|
|
68
|
+
const ariaSelected = await pluginsTab.getAttribute("aria-selected")
|
|
69
|
+
expect(ariaSelected).toBe("true")
|
|
70
|
+
|
|
71
|
+
const pluginsContent = popoverBody.locator('[role="tabpanel"]:visible').first()
|
|
72
|
+
await expect(pluginsContent).toBeVisible()
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test("status popover closes on escape", async ({ page, gotoSession }) => {
|
|
76
|
+
await gotoSession()
|
|
77
|
+
|
|
78
|
+
const { popoverBody } = await openStatusPopover(page)
|
|
79
|
+
await expect(popoverBody).toBeVisible()
|
|
80
|
+
|
|
81
|
+
await page.keyboard.press("Escape")
|
|
82
|
+
await expect(popoverBody).toHaveCount(0)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
test("status popover closes when clicking outside", async ({ page, gotoSession }) => {
|
|
86
|
+
await gotoSession()
|
|
87
|
+
|
|
88
|
+
const { popoverBody } = await openStatusPopover(page)
|
|
89
|
+
await expect(popoverBody).toBeVisible()
|
|
90
|
+
|
|
91
|
+
await page.getByRole("main").click({ position: { x: 5, y: 5 } })
|
|
92
|
+
|
|
93
|
+
await expect(popoverBody).toHaveCount(0)
|
|
94
|
+
})
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { test, expect } from "../fixtures"
|
|
2
|
+
import { waitTerminalFocusIdle, waitTerminalReady } from "../actions"
|
|
3
|
+
import { promptSelector, terminalSelector } from "../selectors"
|
|
4
|
+
import { terminalToggleKey } from "../utils"
|
|
5
|
+
|
|
6
|
+
test("smoke terminal mounts and can create a second tab", async ({ page, gotoSession }) => {
|
|
7
|
+
await gotoSession()
|
|
8
|
+
|
|
9
|
+
const terminals = page.locator(terminalSelector)
|
|
10
|
+
const tabs = page.locator('#terminal-panel [data-slot="tabs-trigger"]')
|
|
11
|
+
const opened = await terminals.first().isVisible()
|
|
12
|
+
|
|
13
|
+
if (!opened) {
|
|
14
|
+
await page.keyboard.press(terminalToggleKey)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
await waitTerminalFocusIdle(page, { term: terminals.first() })
|
|
18
|
+
await expect(terminals).toHaveCount(1)
|
|
19
|
+
|
|
20
|
+
// Ghostty captures a lot of keybinds when focused; move focus back
|
|
21
|
+
// to the app shell before triggering `terminal.new`.
|
|
22
|
+
await page.locator(promptSelector).click()
|
|
23
|
+
await page.keyboard.press("Control+Alt+T")
|
|
24
|
+
|
|
25
|
+
await expect(tabs).toHaveCount(2)
|
|
26
|
+
await expect(terminals).toHaveCount(1)
|
|
27
|
+
await waitTerminalReady(page, { term: terminals.first() })
|
|
28
|
+
})
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Page } from "@playwright/test"
|
|
2
|
+
import { disconnectTerminal, runTerminal, terminalConnects, waitTerminalReady } from "../actions"
|
|
3
|
+
import { test, expect } from "../fixtures"
|
|
4
|
+
import { terminalSelector } from "../selectors"
|
|
5
|
+
import { terminalToggleKey } from "../utils"
|
|
6
|
+
|
|
7
|
+
async function open(page: Page) {
|
|
8
|
+
const term = page.locator(terminalSelector).first()
|
|
9
|
+
const visible = await term.isVisible().catch(() => false)
|
|
10
|
+
if (!visible) await page.keyboard.press(terminalToggleKey)
|
|
11
|
+
await waitTerminalReady(page, { term })
|
|
12
|
+
return term
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
test("terminal reconnects without replacing the pty", async ({ page, withProject }) => {
|
|
16
|
+
await withProject(async ({ gotoSession }) => {
|
|
17
|
+
const name = `REIGNCODE_E2E_RECONNECT_${Date.now()}`
|
|
18
|
+
const token = `E2E_RECONNECT_${Date.now()}`
|
|
19
|
+
|
|
20
|
+
await gotoSession()
|
|
21
|
+
|
|
22
|
+
const term = await open(page)
|
|
23
|
+
const id = await term.getAttribute("data-pty-id")
|
|
24
|
+
if (!id) throw new Error("Active terminal missing data-pty-id")
|
|
25
|
+
|
|
26
|
+
const prev = await terminalConnects(page, { term })
|
|
27
|
+
|
|
28
|
+
await runTerminal(page, {
|
|
29
|
+
term,
|
|
30
|
+
cmd: `export ${name}=${token}; echo ${token}`,
|
|
31
|
+
token,
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
await disconnectTerminal(page, { term })
|
|
35
|
+
|
|
36
|
+
await expect.poll(() => terminalConnects(page, { term }), { timeout: 15_000 }).toBeGreaterThan(prev)
|
|
37
|
+
await expect.poll(() => term.getAttribute("data-pty-id"), { timeout: 5_000 }).toBe(id)
|
|
38
|
+
|
|
39
|
+
await runTerminal(page, {
|
|
40
|
+
term,
|
|
41
|
+
cmd: `echo $${name}`,
|
|
42
|
+
token,
|
|
43
|
+
timeout: 15_000,
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
})
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import type { Page } from "@playwright/test"
|
|
2
|
+
import { runTerminal, waitTerminalReady } from "../actions"
|
|
3
|
+
import { test, expect } from "../fixtures"
|
|
4
|
+
import { dropdownMenuContentSelector, terminalSelector } from "../selectors"
|
|
5
|
+
import { terminalToggleKey, workspacePersistKey } from "../utils"
|
|
6
|
+
|
|
7
|
+
type State = {
|
|
8
|
+
active?: string
|
|
9
|
+
all: Array<{
|
|
10
|
+
id: string
|
|
11
|
+
title: string
|
|
12
|
+
titleNumber: number
|
|
13
|
+
buffer?: string
|
|
14
|
+
}>
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function open(page: Page) {
|
|
18
|
+
const terminal = page.locator(terminalSelector)
|
|
19
|
+
const visible = await terminal.isVisible().catch(() => false)
|
|
20
|
+
if (!visible) await page.keyboard.press(terminalToggleKey)
|
|
21
|
+
await waitTerminalReady(page, { term: terminal })
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function store(page: Page, key: string) {
|
|
25
|
+
return page.evaluate((key) => {
|
|
26
|
+
const raw = localStorage.getItem(key)
|
|
27
|
+
if (raw) return JSON.parse(raw) as State
|
|
28
|
+
|
|
29
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
30
|
+
const next = localStorage.key(i)
|
|
31
|
+
if (!next?.endsWith(":workspace:terminal")) continue
|
|
32
|
+
const value = localStorage.getItem(next)
|
|
33
|
+
if (!value) continue
|
|
34
|
+
return JSON.parse(value) as State
|
|
35
|
+
}
|
|
36
|
+
}, key)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
test("inactive terminal tab buffers persist across tab switches", async ({ page, withProject }) => {
|
|
40
|
+
await withProject(async ({ directory, gotoSession }) => {
|
|
41
|
+
const key = workspacePersistKey(directory, "terminal")
|
|
42
|
+
const one = `E2E_TERM_ONE_${Date.now()}`
|
|
43
|
+
const two = `E2E_TERM_TWO_${Date.now()}`
|
|
44
|
+
const tabs = page.locator('#terminal-panel [data-slot="tabs-trigger"]')
|
|
45
|
+
const first = tabs.filter({ hasText: /Terminal 1/ }).first()
|
|
46
|
+
const second = tabs.filter({ hasText: /Terminal 2/ }).first()
|
|
47
|
+
|
|
48
|
+
await gotoSession()
|
|
49
|
+
await open(page)
|
|
50
|
+
|
|
51
|
+
await runTerminal(page, { cmd: `echo ${one}`, token: one })
|
|
52
|
+
|
|
53
|
+
await page.getByRole("button", { name: /new terminal/i }).click()
|
|
54
|
+
await expect(tabs).toHaveCount(2)
|
|
55
|
+
|
|
56
|
+
await runTerminal(page, { cmd: `echo ${two}`, token: two })
|
|
57
|
+
|
|
58
|
+
await first.click()
|
|
59
|
+
await expect(first).toHaveAttribute("aria-selected", "true")
|
|
60
|
+
|
|
61
|
+
await expect
|
|
62
|
+
.poll(
|
|
63
|
+
async () => {
|
|
64
|
+
const state = await store(page, key)
|
|
65
|
+
const first = state?.all.find((item) => item.titleNumber === 1)?.buffer ?? ""
|
|
66
|
+
const second = state?.all.find((item) => item.titleNumber === 2)?.buffer ?? ""
|
|
67
|
+
return {
|
|
68
|
+
first: first.includes(one),
|
|
69
|
+
second: second.includes(two),
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
{ timeout: 5_000 },
|
|
73
|
+
)
|
|
74
|
+
.toEqual({ first: false, second: true })
|
|
75
|
+
|
|
76
|
+
await second.click()
|
|
77
|
+
await expect(second).toHaveAttribute("aria-selected", "true")
|
|
78
|
+
await expect
|
|
79
|
+
.poll(
|
|
80
|
+
async () => {
|
|
81
|
+
const state = await store(page, key)
|
|
82
|
+
const first = state?.all.find((item) => item.titleNumber === 1)?.buffer ?? ""
|
|
83
|
+
const second = state?.all.find((item) => item.titleNumber === 2)?.buffer ?? ""
|
|
84
|
+
return {
|
|
85
|
+
first: first.includes(one),
|
|
86
|
+
second: second.includes(two),
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
{ timeout: 5_000 },
|
|
90
|
+
)
|
|
91
|
+
.toEqual({ first: true, second: false })
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test("closing the active terminal tab falls back to the previous tab", async ({ page, withProject }) => {
|
|
96
|
+
await withProject(async ({ directory, gotoSession }) => {
|
|
97
|
+
const key = workspacePersistKey(directory, "terminal")
|
|
98
|
+
const tabs = page.locator('#terminal-panel [data-slot="tabs-trigger"]')
|
|
99
|
+
|
|
100
|
+
await gotoSession()
|
|
101
|
+
await open(page)
|
|
102
|
+
|
|
103
|
+
await page.getByRole("button", { name: /new terminal/i }).click()
|
|
104
|
+
await expect(tabs).toHaveCount(2)
|
|
105
|
+
|
|
106
|
+
const second = tabs.filter({ hasText: /Terminal 2/ }).first()
|
|
107
|
+
await second.click()
|
|
108
|
+
await expect(second).toHaveAttribute("aria-selected", "true")
|
|
109
|
+
|
|
110
|
+
await second.hover()
|
|
111
|
+
await page
|
|
112
|
+
.getByRole("button", { name: /close terminal/i })
|
|
113
|
+
.nth(1)
|
|
114
|
+
.click({ force: true })
|
|
115
|
+
|
|
116
|
+
const first = tabs.filter({ hasText: /Terminal 1/ }).first()
|
|
117
|
+
await expect(tabs).toHaveCount(1)
|
|
118
|
+
await expect(first).toHaveAttribute("aria-selected", "true")
|
|
119
|
+
await expect
|
|
120
|
+
.poll(
|
|
121
|
+
async () => {
|
|
122
|
+
const state = await store(page, key)
|
|
123
|
+
return {
|
|
124
|
+
count: state?.all.length ?? 0,
|
|
125
|
+
first: state?.all.some((item) => item.titleNumber === 1) ?? false,
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
{ timeout: 15_000 },
|
|
129
|
+
)
|
|
130
|
+
.toEqual({ count: 1, first: true })
|
|
131
|
+
})
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
test("terminal tab can be renamed from the context menu", async ({ page, withProject }) => {
|
|
135
|
+
await withProject(async ({ directory, gotoSession }) => {
|
|
136
|
+
const key = workspacePersistKey(directory, "terminal")
|
|
137
|
+
const rename = `E2E term ${Date.now()}`
|
|
138
|
+
const tab = page.locator('#terminal-panel [data-slot="tabs-trigger"]').first()
|
|
139
|
+
|
|
140
|
+
await gotoSession()
|
|
141
|
+
await open(page)
|
|
142
|
+
|
|
143
|
+
await expect(tab).toContainText(/Terminal 1/)
|
|
144
|
+
await tab.click({ button: "right" })
|
|
145
|
+
|
|
146
|
+
const menu = page.locator(dropdownMenuContentSelector).first()
|
|
147
|
+
await expect(menu).toBeVisible()
|
|
148
|
+
await menu.getByRole("menuitem", { name: /^Rename$/i }).click()
|
|
149
|
+
await expect(menu).toHaveCount(0)
|
|
150
|
+
|
|
151
|
+
const input = page.locator('#terminal-panel input[type="text"]').first()
|
|
152
|
+
await expect(input).toBeVisible()
|
|
153
|
+
await input.fill(rename)
|
|
154
|
+
await input.press("Enter")
|
|
155
|
+
|
|
156
|
+
await expect(input).toHaveCount(0)
|
|
157
|
+
await expect(tab).toContainText(rename)
|
|
158
|
+
await expect
|
|
159
|
+
.poll(
|
|
160
|
+
async () => {
|
|
161
|
+
const state = await store(page, key)
|
|
162
|
+
return state?.all[0]?.title
|
|
163
|
+
},
|
|
164
|
+
{ timeout: 5_000 },
|
|
165
|
+
)
|
|
166
|
+
.toBe(rename)
|
|
167
|
+
})
|
|
168
|
+
})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { test, expect } from "../fixtures"
|
|
2
|
+
import { waitTerminalReady } from "../actions"
|
|
3
|
+
import { terminalSelector } from "../selectors"
|
|
4
|
+
import { terminalToggleKey } from "../utils"
|
|
5
|
+
|
|
6
|
+
test("terminal panel can be toggled", async ({ page, gotoSession }) => {
|
|
7
|
+
await gotoSession()
|
|
8
|
+
|
|
9
|
+
const terminal = page.locator(terminalSelector)
|
|
10
|
+
const initiallyOpen = await terminal.isVisible()
|
|
11
|
+
if (initiallyOpen) {
|
|
12
|
+
await page.keyboard.press(terminalToggleKey)
|
|
13
|
+
await expect(terminal).toHaveCount(0)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
await page.keyboard.press(terminalToggleKey)
|
|
17
|
+
await waitTerminalReady(page, { term: terminal })
|
|
18
|
+
})
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { test, expect } from "./fixtures"
|
|
2
|
+
import { modelVariantCycleSelector } from "./selectors"
|
|
3
|
+
|
|
4
|
+
test("smoke model variant cycle updates label", async ({ page, gotoSession }) => {
|
|
5
|
+
await gotoSession()
|
|
6
|
+
|
|
7
|
+
await page.addStyleTag({
|
|
8
|
+
content: `${modelVariantCycleSelector} { display: inline-block !important; }`,
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
const button = page.locator(modelVariantCycleSelector)
|
|
12
|
+
const exists = (await button.count()) > 0
|
|
13
|
+
test.skip(!exists, "current model has no variants")
|
|
14
|
+
if (!exists) return
|
|
15
|
+
|
|
16
|
+
await expect(button).toBeVisible()
|
|
17
|
+
|
|
18
|
+
const before = (await button.innerText()).trim()
|
|
19
|
+
await button.click()
|
|
20
|
+
await expect(button).not.toHaveText(before)
|
|
21
|
+
|
|
22
|
+
const after = (await button.innerText()).trim()
|
|
23
|
+
await button.click()
|
|
24
|
+
await expect(button).not.toHaveText(after)
|
|
25
|
+
})
|
package/e2e/utils.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { createOpencodeClient } from "@reign-labs/sdk/v2/client"
|
|
2
|
+
import { base64Encode, checksum } from "@reign-labs/util/encode"
|
|
3
|
+
|
|
4
|
+
export const serverHost = process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"
|
|
5
|
+
export const serverPort = process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"
|
|
6
|
+
|
|
7
|
+
export const serverUrl = `http://${serverHost}:${serverPort}`
|
|
8
|
+
export const serverName = `${serverHost}:${serverPort}`
|
|
9
|
+
|
|
10
|
+
const localHosts = ["127.0.0.1", "localhost"]
|
|
11
|
+
|
|
12
|
+
const serverLabels = (() => {
|
|
13
|
+
const url = new URL(serverUrl)
|
|
14
|
+
if (!localHosts.includes(url.hostname)) return [serverName]
|
|
15
|
+
return localHosts.map((host) => `${host}:${url.port}`)
|
|
16
|
+
})()
|
|
17
|
+
|
|
18
|
+
export const serverNames = [...new Set(serverLabels)]
|
|
19
|
+
|
|
20
|
+
export const serverUrls = serverNames.map((name) => `http://${name}`)
|
|
21
|
+
|
|
22
|
+
const escape = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
23
|
+
|
|
24
|
+
export const serverNamePattern = new RegExp(`(?:${serverNames.map(escape).join("|")})`)
|
|
25
|
+
|
|
26
|
+
export const modKey = process.platform === "darwin" ? "Meta" : "Control"
|
|
27
|
+
export const terminalToggleKey = "Control+Backquote"
|
|
28
|
+
|
|
29
|
+
export function createSdk(directory?: string) {
|
|
30
|
+
return createOpencodeClient({ baseUrl: serverUrl, directory, throwOnError: true })
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function resolveDirectory(directory: string) {
|
|
34
|
+
return createSdk(directory)
|
|
35
|
+
.path.get()
|
|
36
|
+
.then((x) => x.data?.directory ?? directory)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function getWorktree() {
|
|
40
|
+
const sdk = createSdk()
|
|
41
|
+
const result = await sdk.path.get()
|
|
42
|
+
const data = result.data
|
|
43
|
+
if (!data?.worktree) throw new Error(`Failed to resolve a worktree from ${serverUrl}/path`)
|
|
44
|
+
return data.worktree
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function dirSlug(directory: string) {
|
|
48
|
+
return base64Encode(directory)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function dirPath(directory: string) {
|
|
52
|
+
return `/${dirSlug(directory)}`
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function sessionPath(directory: string, sessionID?: string) {
|
|
56
|
+
return `${dirPath(directory)}/session${sessionID ? `/${sessionID}` : ""}`
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function workspacePersistKey(directory: string, key: string) {
|
|
60
|
+
const head = (directory.slice(0, 12) || "workspace").replace(/[^a-zA-Z0-9._-]/g, "-")
|
|
61
|
+
const sum = checksum(directory) ?? "0"
|
|
62
|
+
return `opencode.workspace.${head}.${sum}.dat:workspace:${key}`
|
|
63
|
+
}
|