mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-13 04:59:58 -04:00
Compare commits
70 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 939c2c2411 | |||
| 56973e0ca4 | |||
| c253d4d311 | |||
| 33a1bd2e90 | |||
| 20197c6b8a | |||
| 230b2f8488 | |||
| a78c8d8972 | |||
| bac3631ded | |||
| 664b0ba2d6 | |||
| 9c8b50b989 | |||
| 4358a02cf4 | |||
| a31058d76c | |||
| 91ba9f2412 | |||
| 3125f67708 | |||
| 550e9cc636 | |||
| af127a643b | |||
| 77aa1cfede | |||
| a2ed936283 | |||
| b17fbf41e3 | |||
| 9d6e05b6e4 | |||
| 9b805c140f | |||
| d31a994c27 | |||
| 76640a5c9c | |||
| 76dbaf20ad | |||
| 9dd0e39867 | |||
| 69a465d33b | |||
| 66c2967520 | |||
| 883c9e3cb4 | |||
| 0d703d39e7 | |||
| bc2251d6d8 | |||
| b034154e09 | |||
| c894f87771 | |||
| fc5c781085 | |||
| f6aa1a67f0 | |||
| 0859f77153 | |||
| 7a22ac865d | |||
| b75dd58f7c | |||
| 56197e621a | |||
| d8f62cfdcb | |||
| c9539979bd | |||
| f5e6b8c769 | |||
| 2025ed939a | |||
| a7642737c7 | |||
| c081e12c60 | |||
| 8d5dd206a6 | |||
| c6a86acb0b | |||
| c7bee09632 | |||
| a3625cd2cb | |||
| 0dec446ee6 | |||
| dff6eb631b | |||
| 3251bff6ff | |||
| f713d39c61 | |||
| c80472b6d5 | |||
| c86b3079ac | |||
| f8f50ee375 | |||
| 912a1c4e8a | |||
| a841fc22bb | |||
| 723f5e0028 | |||
| 17cae23dce | |||
| fd30b9765d | |||
| a20d945245 | |||
| 10ebf70a07 | |||
| b50924b993 | |||
| b24b1b3f16 | |||
| 930b0751b1 | |||
| f06a86eeac | |||
| 653b7d79cd | |||
| 70ce0d0970 | |||
| 0777e84598 | |||
| bef795b2fe |
@@ -10,6 +10,7 @@ kitlangton
|
||||
kommander
|
||||
ludvigrask
|
||||
MrMushrooooom
|
||||
neriousy
|
||||
nexxeln
|
||||
R44VC0RP
|
||||
rekram1-node
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
name: deploy-lab-catalog
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [v2]
|
||||
paths:
|
||||
- ".github/workflows/deploy-lab-catalog.yml"
|
||||
- "bun.lock"
|
||||
- "package.json"
|
||||
- "packages/drive/**"
|
||||
- "packages/protocol/src/simulation.ts"
|
||||
- "packages/simulation/**"
|
||||
- "packages/lab/catalog/**"
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: deploy-lab-catalog-${{ github.ref_name }}
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
if: github.repository == 'anomalyco/opencode' && github.ref_name == 'v2'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
|
||||
- uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Install ffmpeg
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install --yes ffmpeg
|
||||
|
||||
- name: Validate
|
||||
run: |
|
||||
bun --cwd packages/protocol typecheck
|
||||
bun --cwd packages/simulation typecheck
|
||||
bun --cwd packages/drive run check
|
||||
bun --cwd packages/drive run test
|
||||
bun --cwd packages/lab/catalog run check
|
||||
|
||||
- name: Deploy
|
||||
working-directory: packages/lab/catalog
|
||||
run: bun run deploy
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
@@ -55,6 +55,12 @@ jobs:
|
||||
git config --global user.email "bot@opencode.ai"
|
||||
git config --global user.name "opencode"
|
||||
|
||||
- name: Install ffmpeg
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install --yes ffmpeg
|
||||
|
||||
- name: Cache Turbo
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
@@ -66,7 +72,7 @@ jobs:
|
||||
|
||||
- name: Run unit tests
|
||||
timeout-minutes: 20
|
||||
run: GITHUB_ACTIONS=false bun turbo test
|
||||
run: GITHUB_ACTIONS=false bun turbo test ${{ runner.os == 'Windows' && '--filter=!opencode-drive' || '' }}
|
||||
env:
|
||||
OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: ${{ runner.os == 'Windows' && 'true' || 'false' }}
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ files. If the script is unsuccessful, automatically fix the script and run it ag
|
||||
Scripts use one typed definition object. `setup` runs before OpenCode starts,
|
||||
and `fs.writeFile` always writes inside the simulated project.
|
||||
|
||||
You can read the full typed API here: https://raw.githubusercontent.com/jlongster/opencode-drive/refs/heads/main/src/script/types.ts
|
||||
You can read the full typed API here: https://raw.githubusercontent.com/anomalyco/opencode/v2/packages/drive/src/script/types.ts
|
||||
|
||||
```ts
|
||||
import { defineScript } from "opencode-drive"
|
||||
@@ -83,7 +83,7 @@ itself (this is extremely rare, do not use this unless explicitly asked). In thi
|
||||
mode `ui` is typed as `null`; call `server.launch()` exactly
|
||||
once before launching clients. Each `clients.launch(name)` result provides the
|
||||
same UI methods as the automatic client. You can see an example of this API
|
||||
here: https://raw.githubusercontent.com/jlongster/opencode-drive/refs/heads/main/examples/multiple-clients.ts
|
||||
here: https://raw.githubusercontent.com/anomalyco/opencode/v2/packages/drive/examples/multiple-clients.ts
|
||||
|
||||
Use the exported `wait(milliseconds)` utility for an unconditional delay.
|
||||
|
||||
@@ -114,8 +114,8 @@ completion are automatic.
|
||||
|
||||
You can see some example scripts here:
|
||||
|
||||
- https://raw.githubusercontent.com/jlongster/opencode-drive/refs/heads/main/examples/simple.ts
|
||||
- https://raw.githubusercontent.com/jlongster/opencode-drive/refs/heads/main/examples/serve.ts
|
||||
- https://raw.githubusercontent.com/anomalyco/opencode/v2/packages/drive/examples/simple.ts
|
||||
- https://raw.githubusercontent.com/anomalyco/opencode/v2/packages/drive/examples/serve.ts
|
||||
|
||||
## Prune
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { tool } from "@opencode-ai/plugin"
|
||||
const TEAM = {
|
||||
tui: ["kommander", "simonklee"],
|
||||
desktop_web: ["Hona", "Brendonovich"],
|
||||
core: ["jlongster", "rekram1-node", "nexxeln", "kitlangton"],
|
||||
core: ["jlongster", "rekram1-node", "neriousy", "nexxeln", "kitlangton"],
|
||||
inference: ["fwang", "MrMushrooooom", "starptech"],
|
||||
windows: ["Hona"],
|
||||
} as const
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
- After changing the public Protocol or Server `HttpApi`, run `bun run generate` from `packages/client`. Do not edit `src/generated` or `src/generated-effect` directly.
|
||||
- Keep runtime dependencies directed from Schema to Core and Protocol, then from Core and Protocol to Server. Client runtime code may depend on Schema and Protocol but never Core or Server; `sdk-next` composes Client, Core, and Server.
|
||||
- Do not modify `packages/opencode` unless the user explicitly asks for V1 work. `packages/opencode` is the V1 implementation and is present for reference only. New implementation changes should land in the V2 package set: `packages/core`, `packages/cli`, `packages/server`, `packages/protocol`, `packages/schema`, and related generated client surfaces when required.
|
||||
- The default branch in this repo is `dev`.
|
||||
- Local `main` ref may not exist; use `dev` or `origin/dev` for diffs.
|
||||
- The default branch in this repo is `v2`.
|
||||
- Base all new branches and worktrees on `v2`, or `origin/v2` when the local `v2` ref is unavailable. Do not base them on `dev`.
|
||||
- Local `main` ref may not exist; use `v2` or `origin/v2` for diffs.
|
||||
|
||||
## Live V2 TUI Testing
|
||||
|
||||
|
||||
+10
-7
@@ -33,6 +33,7 @@
|
||||
"packages": [
|
||||
"packages/*",
|
||||
"packages/console/*",
|
||||
"packages/lab/*",
|
||||
"packages/stats/*",
|
||||
"packages/slack"
|
||||
],
|
||||
@@ -46,9 +47,9 @@
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@hono/standard-validator": "0.2.0",
|
||||
"@hono/zod-validator": "0.4.2",
|
||||
"@opentui/core": "0.0.0-20260808-9ecf7c0a",
|
||||
"@opentui/keymap": "0.0.0-20260808-9ecf7c0a",
|
||||
"@opentui/solid": "0.0.0-20260808-9ecf7c0a",
|
||||
"@opentui/core": "0.5.2",
|
||||
"@opentui/keymap": "0.5.2",
|
||||
"@opentui/solid": "0.5.2",
|
||||
"@tanstack/solid-virtual": "3.13.32",
|
||||
"@shikijs/stream": "4.2.0",
|
||||
"@standard-schema/spec": "1.1.0",
|
||||
@@ -120,7 +121,8 @@
|
||||
"prettier": "3.6.2",
|
||||
"semver": "^7.6.0",
|
||||
"sst": "catalog:",
|
||||
"turbo": "2.10.2"
|
||||
"turbo": "2.10.2",
|
||||
"vitest": "4.1.10"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "3.933.0",
|
||||
@@ -150,9 +152,9 @@
|
||||
"electron"
|
||||
],
|
||||
"overrides": {
|
||||
"@opentui/core": "0.0.0-20260808-9ecf7c0a",
|
||||
"@opentui/keymap": "0.0.0-20260808-9ecf7c0a",
|
||||
"@opentui/solid": "0.0.0-20260808-9ecf7c0a",
|
||||
"@opentui/core": "catalog:",
|
||||
"@opentui/keymap": "catalog:",
|
||||
"@opentui/solid": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"effect": "catalog:"
|
||||
@@ -172,6 +174,7 @@
|
||||
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
|
||||
"effect@4.0.0-beta.101": "patches/effect@4.0.0-beta.101.patch",
|
||||
"@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch",
|
||||
"@cloudflare/vitest-pool-workers@0.12.6": "patches/@cloudflare%2Fvitest-pool-workers@0.12.6.patch",
|
||||
"@ff-labs/fff-bun@0.10.1": "patches/@ff-labs%2Ffff-bun@0.10.1.patch"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,8 +86,8 @@ function TargetServerRoute(props: ParentProps) {
|
||||
return (
|
||||
// Owns the server-identity remount. Session changes must not remount this subtree.
|
||||
<Show when={requireServerKey(params.serverKey)} keyed>
|
||||
<ServerSDKProvider server={conn}>
|
||||
<ServerSyncProvider server={conn}>{props.children}</ServerSyncProvider>
|
||||
<ServerSDKProvider server={conn()}>
|
||||
<ServerSyncProvider server={conn()}>{props.children}</ServerSyncProvider>
|
||||
</ServerSDKProvider>
|
||||
</Show>
|
||||
)
|
||||
@@ -130,16 +130,14 @@ function DraftRoute() {
|
||||
function ResolvedDraftRoute(props: { draft: DraftTab }) {
|
||||
const global = useGlobal()
|
||||
const conn = createMemo(() => global.servers.list().find((item) => ServerConnection.key(item) === props.draft.server))
|
||||
const directory = () => props.draft.directory
|
||||
const serverKey = () => props.draft.server
|
||||
|
||||
return (
|
||||
<Show when={`${props.draft.server}\0${props.draft.directory}`} keyed>
|
||||
<ServerSDKProvider server={conn}>
|
||||
<ServerSyncProvider server={conn}>
|
||||
<ModelsProvider directory={directory}>
|
||||
<SDKProvider directory={directory}>
|
||||
<DirectoryDataProvider directory={directory} server={serverKey}>
|
||||
<ServerSDKProvider server={conn()}>
|
||||
<ServerSyncProvider server={conn()}>
|
||||
<ModelsProvider directory={props.draft.directory}>
|
||||
<SDKProvider directory={props.draft.directory}>
|
||||
<DirectoryDataProvider directory={props.draft.directory} server={props.draft.server}>
|
||||
<DraftProviders>
|
||||
<NewSession />
|
||||
</DraftProviders>
|
||||
@@ -237,7 +235,7 @@ function DesktopCommands() {
|
||||
}
|
||||
|
||||
type ServerScopedShellProps = ParentProps<{
|
||||
directory?: () => string | undefined
|
||||
directory?: string
|
||||
serverScoped?: JSX.Element
|
||||
}>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useIsRouting, useLocation } from "@solidjs/router"
|
||||
import { batch, createEffect, onCleanup, onMount } from "solid-js"
|
||||
import { batch, createEffect, onCleanup, onMount, Show } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { makeEventListener } from "@solid-primitives/event-listener"
|
||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||
@@ -571,7 +571,7 @@ export function DebugBar(props: { inline?: boolean } = {}) {
|
||||
value={language.t(`debugBar.direction.${language.direction()}`)}
|
||||
onClick={() => language.setDirection(language.direction() === "rtl" ? "ltr" : "rtl")}
|
||||
/>
|
||||
{platform.setForceFocus && (
|
||||
<Show when={platform.setForceFocus}>
|
||||
<ToggleCell
|
||||
active={state.focus}
|
||||
inline={props.inline}
|
||||
@@ -580,7 +580,7 @@ export function DebugBar(props: { inline?: boolean } = {}) {
|
||||
value={language.t(state.focus ? "debugBar.focus.on" : "debugBar.focus.off")}
|
||||
onClick={() => void toggleFocus()}
|
||||
/>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
</aside>
|
||||
)
|
||||
|
||||
@@ -12,7 +12,7 @@ import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
|
||||
import { DialogBody, DialogHeader, DialogTitle, DialogV2 } from "@opencode-ai/ui/v2/dialog-v2"
|
||||
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
|
||||
import { showToast } from "@/utils/toast"
|
||||
import { type Accessor, type Component, createMemo, createUniqueId, For, Match, onMount, Show, Switch } from "solid-js"
|
||||
import { type Component, createMemo, createUniqueId, For, Match, onMount, Show, Switch } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { useParams } from "@solidjs/router"
|
||||
import { ExternalLink } from "@/components/external-link"
|
||||
@@ -40,7 +40,7 @@ export function useProviderConnectController(options: { onBack?: () => void } =
|
||||
}
|
||||
|
||||
export const DialogConnectProvider: Component<{
|
||||
directory?: Accessor<string | undefined>
|
||||
directory?: string
|
||||
controller?: ReturnType<typeof useProviderConnectController>
|
||||
}> = (props) => {
|
||||
const fallback = useProviderConnectController()
|
||||
@@ -136,15 +136,11 @@ export const DialogConnectProvider: Component<{
|
||||
)
|
||||
}
|
||||
|
||||
function ProviderPicker(props: {
|
||||
directory?: Accessor<string | undefined>
|
||||
onSelect: (provider: string) => void
|
||||
onPrepare?: () => void
|
||||
}) {
|
||||
function ProviderPicker(props: { directory?: string; onSelect: (provider: string) => void; onPrepare?: () => void }) {
|
||||
const settings = useSettings()
|
||||
if (settings.general.newLayoutDesigns())
|
||||
return <ProviderPickerV2 directory={props.directory} onSelect={props.onSelect} onPrepare={props.onPrepare} />
|
||||
const providers = useProviders(() => props.directory?.())
|
||||
const providers = useProviders(() => props.directory)
|
||||
const language = useLanguage()
|
||||
const popularGroup = () => language.t("dialog.provider.group.popular")
|
||||
const otherGroup = () => language.t("dialog.provider.group.other")
|
||||
@@ -211,12 +207,8 @@ function ProviderPicker(props: {
|
||||
)
|
||||
}
|
||||
|
||||
function ProviderPickerV2(props: {
|
||||
directory?: Accessor<string | undefined>
|
||||
onSelect: (provider: string) => void
|
||||
onPrepare?: () => void
|
||||
}) {
|
||||
const providers = useProviders(() => props.directory?.())
|
||||
function ProviderPickerV2(props: { directory?: string; onSelect: (provider: string) => void; onPrepare?: () => void }) {
|
||||
const providers = useProviders(() => props.directory)
|
||||
const language = useLanguage()
|
||||
const [store, setStore] = createStore({
|
||||
filter: "",
|
||||
@@ -364,7 +356,7 @@ function ProviderPickerV2(props: {
|
||||
|
||||
function ProviderConnection(props: {
|
||||
provider: string
|
||||
directory?: Accessor<string | undefined>
|
||||
directory?: string
|
||||
onBack: () => void
|
||||
setBack: (handler: () => void) => void
|
||||
}) {
|
||||
@@ -374,8 +366,8 @@ function ProviderConnection(props: {
|
||||
const language = useLanguage()
|
||||
const settings = useSettings()
|
||||
const newLayout = settings.general.newLayoutDesigns
|
||||
const providers = useProviders(() => props.directory?.())
|
||||
const directory = () => props.directory?.() ?? decode64(params.dir)
|
||||
const providers = useProviders(() => props.directory)
|
||||
const directory = () => props.directory ?? decode64(params.dir)
|
||||
|
||||
const provider = createMemo(
|
||||
() => providers.all().get(props.provider) ?? serverSync().data.provider.all.get(props.provider)!,
|
||||
|
||||
@@ -31,7 +31,7 @@ export const DialogManageModels: Component = () => {
|
||||
const directory = () => decode64(local.slug())
|
||||
|
||||
const handleConnectProvider = () => {
|
||||
void dialog.show(() => <DialogConnectProvider directory={directory} />)
|
||||
void dialog.show(() => <DialogConnectProvider directory={directory()} />)
|
||||
}
|
||||
const providerRank = (id: string) => popularProviders.indexOf(id)
|
||||
const providerList = (providerID: string) => local.model.list().filter((x) => x.provider.id === providerID)
|
||||
@@ -123,7 +123,7 @@ export const DialogManageModelsV2: Component = () => {
|
||||
const directory = () => decode64(local.slug())
|
||||
|
||||
const handleConnectProvider = () => {
|
||||
void dialog.show(() => <DialogConnectProvider directory={directory} />)
|
||||
void dialog.show(() => <DialogConnectProvider directory={directory()} />)
|
||||
}
|
||||
const providerList = (providerID: string) => local.model.list().filter((x) => x.provider.id === providerID)
|
||||
const providerVisible = (providerID: string) =>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createSignal } from "solid-js"
|
||||
import { createSignal, Index, Show } from "solid-js"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
@@ -83,61 +83,71 @@ export function DialogReleaseNotes(props: { highlights: Highlight[] }) {
|
||||
{/* Bottom section - buttons and indicators (fixed position) */}
|
||||
<div class="flex flex-col gap-12">
|
||||
<div class="flex flex-col items-start gap-3">
|
||||
{isLast() ? (
|
||||
<Show
|
||||
when={isLast()}
|
||||
fallback={
|
||||
<Button variant="secondary" size="large" onClick={handleNext}>
|
||||
{language.t("dialog.releaseNotes.action.next")}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Button variant="primary" size="large" onClick={handleClose}>
|
||||
{language.t("dialog.releaseNotes.action.getStarted")}
|
||||
</Button>
|
||||
) : (
|
||||
<Button variant="secondary" size="large" onClick={handleNext}>
|
||||
{language.t("dialog.releaseNotes.action.next")}
|
||||
</Button>
|
||||
)}
|
||||
</Show>
|
||||
|
||||
<Button variant="ghost" size="small" onClick={handleDisable}>
|
||||
{language.t("dialog.releaseNotes.action.hideFuture")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{paged() && (
|
||||
<Show when={paged()}>
|
||||
<div class="flex items-center gap-1.5 -my-2.5">
|
||||
{props.highlights.map((_, i) => (
|
||||
<button
|
||||
type="button"
|
||||
class="h-6 flex items-center cursor-pointer bg-transparent border-none p-0 transition-all duration-200"
|
||||
classList={{
|
||||
"w-8": i === index(),
|
||||
"w-3": i !== index(),
|
||||
}}
|
||||
onClick={() => setIndex(i)}
|
||||
>
|
||||
<div
|
||||
class="w-full h-0.5 rounded-[1px] transition-colors duration-200"
|
||||
<Index each={props.highlights}>
|
||||
{(_, i) => (
|
||||
<button
|
||||
type="button"
|
||||
class="h-6 flex items-center cursor-pointer bg-transparent border-none p-0 transition-all duration-200"
|
||||
classList={{
|
||||
"bg-icon-strong-base": i === index(),
|
||||
"bg-icon-weak-base": i !== index(),
|
||||
"w-8": i === index(),
|
||||
"w-3": i !== index(),
|
||||
}}
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
onClick={() => setIndex(i)}
|
||||
>
|
||||
<div
|
||||
class="w-full h-0.5 rounded-[1px] transition-colors duration-200"
|
||||
classList={{
|
||||
"bg-icon-strong-base": i === index(),
|
||||
"bg-icon-weak-base": i !== index(),
|
||||
}}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</Index>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right side - Media content (edge to edge) */}
|
||||
{feature()?.media && (
|
||||
<div class="flex-1 min-w-0 bg-surface-base overflow-hidden rounded-r-xl">
|
||||
{feature()!.media!.type === "image" ? (
|
||||
<img
|
||||
src={feature()!.media!.src}
|
||||
alt={feature()!.media!.alt ?? feature()?.title ?? language.t("dialog.releaseNotes.media.alt")}
|
||||
class="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<video src={feature()!.media!.src} autoplay loop muted playsinline class="w-full h-full object-cover" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<Show when={feature()?.media}>
|
||||
{(media) => (
|
||||
<div class="flex-1 min-w-0 bg-surface-base overflow-hidden rounded-r-xl">
|
||||
<Show
|
||||
when={media().type === "image"}
|
||||
fallback={
|
||||
<video src={media().src} autoplay loop muted playsinline class="w-full h-full object-cover" />
|
||||
}
|
||||
>
|
||||
<img
|
||||
src={media().src}
|
||||
alt={media().alt ?? feature()?.title ?? language.t("dialog.releaseNotes.media.alt")}
|
||||
class="w-full h-full object-cover"
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
</Dialog>
|
||||
)
|
||||
|
||||
@@ -37,7 +37,7 @@ export const DialogSelectModelUnpaidV2: Component<{ model?: ModelState }> = (pro
|
||||
void import("./dialog-connect-provider").then((x) => {
|
||||
const controller = x.useProviderConnectController()
|
||||
controller.select(provider)
|
||||
void dialog.show(() => <x.DialogConnectProvider controller={controller} directory={directory} />)
|
||||
void dialog.show(() => <x.DialogConnectProvider controller={controller} directory={directory()} />)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ export const DialogSelectModelUnpaid: Component<{ model?: ModelState }> = (props
|
||||
void import("./dialog-connect-provider").then((x) => {
|
||||
const controller = x.useProviderConnectController()
|
||||
controller.select(provider)
|
||||
void dialog.show(() => <x.DialogConnectProvider controller={controller} directory={directory} />)
|
||||
void dialog.show(() => <x.DialogConnectProvider controller={controller} directory={directory()} />)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -148,7 +148,7 @@ export function ModelSelectorPopover(props: {
|
||||
const handleConnectProvider = () => {
|
||||
close("provider")
|
||||
void import("./dialog-connect-provider").then((x) => {
|
||||
void dialog.show(() => <x.DialogConnectProvider directory={directory} />)
|
||||
void dialog.show(() => <x.DialogConnectProvider directory={directory()} />)
|
||||
})
|
||||
}
|
||||
const language = useLanguage()
|
||||
@@ -240,7 +240,7 @@ export function ModelSelectorPopoverV2(props: {
|
||||
trigger={props.trigger}
|
||||
models={controller.models}
|
||||
groups={controller.groups}
|
||||
current={controller.current}
|
||||
current={controller.current()}
|
||||
select={controller.select}
|
||||
onManage={() => {
|
||||
void import("./dialog-manage-models").then((module) => {
|
||||
@@ -295,7 +295,7 @@ function ModelSelectorPopoverV2View(props: {
|
||||
trigger: ModelSelectorTrigger
|
||||
models: (search: string) => ModelItem[]
|
||||
groups: (models: ModelItem[]) => { category: string; items: ModelItem[] }[]
|
||||
current: () => string | undefined
|
||||
current: string | undefined
|
||||
select: (item: ModelItem) => void
|
||||
onManage: () => void
|
||||
onClose: () => void
|
||||
@@ -310,7 +310,7 @@ function ModelSelectorPopoverV2View(props: {
|
||||
const groups = createMemo(() => props.groups(models()))
|
||||
const keys = () => [...models().map(modelKey), manageKey]
|
||||
const initialActive = () => {
|
||||
const selected = props.current()
|
||||
const selected = props.current
|
||||
const options = keys()
|
||||
if (selected && options.includes(selected)) return selected
|
||||
return options[0] ?? ""
|
||||
@@ -453,7 +453,7 @@ function ModelSelectorPopoverV2View(props: {
|
||||
<MenuV2.GroupLabel class="gap-2 px-3">
|
||||
<span class="min-w-0 truncate">{group.items[0].provider.name}</span>
|
||||
</MenuV2.GroupLabel>
|
||||
<MenuV2.RadioGroup value={props.current()}>
|
||||
<MenuV2.RadioGroup value={props.current}>
|
||||
<For each={group.items}>
|
||||
{(item) => (
|
||||
<TooltipV2
|
||||
@@ -473,7 +473,7 @@ function ModelSelectorPopoverV2View(props: {
|
||||
<MenuV2.RadioItem
|
||||
value={modelKey(item)}
|
||||
data-option-key={modelKey(item)}
|
||||
data-selected-model={props.current() === modelKey(item) ? true : undefined}
|
||||
data-selected-model={props.current === modelKey(item) ? true : undefined}
|
||||
class="scroll-my-6 w-full"
|
||||
classList={{ "!bg-v2-overlay-simple-overlay-hover": store.active === modelKey(item) }}
|
||||
onMouseEnter={() => {
|
||||
@@ -529,7 +529,7 @@ export const DialogSelectModel: Component<{ provider?: string; model?: ModelStat
|
||||
|
||||
const provider = () => {
|
||||
void import("./dialog-connect-provider").then((x) => {
|
||||
void dialog.show(() => <x.DialogConnectProvider directory={directory} />)
|
||||
void dialog.show(() => <x.DialogConnectProvider directory={directory()} />)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -96,13 +96,16 @@ export function ServerRow(props: ServerRowProps) {
|
||||
{(conn) => (
|
||||
<div class="flex flex-row gap-3">
|
||||
<span>
|
||||
{conn().http.username ? (
|
||||
<Show
|
||||
when={conn().http.username}
|
||||
fallback={<span class="text-text-weaker">{language.t("server.row.noUsername")}</span>}
|
||||
>
|
||||
<span class="text-text-weak">{conn().http.username}</span>
|
||||
) : (
|
||||
<span class="text-text-weaker">{language.t("server.row.noUsername")}</span>
|
||||
)}
|
||||
</Show>
|
||||
</span>
|
||||
{conn().http.password && <span class="text-text-weak">••••••••</span>}
|
||||
<Show when={conn().http.password}>
|
||||
<span class="text-text-weak">••••••••</span>
|
||||
</Show>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
|
||||
@@ -12,7 +12,7 @@ import { FileVisual } from "./session-sortable-tab"
|
||||
|
||||
export function SortableTabV2(props: {
|
||||
tab: string
|
||||
index: () => number
|
||||
index: number
|
||||
temporary?: boolean
|
||||
onTabClose: (tab: string) => void
|
||||
onTabDoubleClick?: (tab: string) => void
|
||||
@@ -26,7 +26,7 @@ export function SortableTabV2(props: {
|
||||
return props.tab
|
||||
},
|
||||
get index() {
|
||||
return props.index()
|
||||
return props.index
|
||||
},
|
||||
})
|
||||
const path = createMemo(() => file.pathFromTab(props.tab))
|
||||
|
||||
@@ -14,7 +14,7 @@ import { focusTerminalById } from "@/pages/session/helpers"
|
||||
|
||||
export function SortableTerminalTabV2(props: {
|
||||
terminal: LocalPTY
|
||||
index: () => number
|
||||
index: number
|
||||
newLayout: boolean
|
||||
onClose?: () => void
|
||||
}): JSX.Element {
|
||||
@@ -25,7 +25,7 @@ export function SortableTerminalTabV2(props: {
|
||||
return props.terminal.id
|
||||
},
|
||||
get index() {
|
||||
return props.index()
|
||||
return props.index
|
||||
},
|
||||
})
|
||||
const [store, setStore] = createStore({
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Component, For, Show, createMemo, lazy, onCleanup, onMount } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { Dynamic } from "solid-js/web"
|
||||
import { makeEventListener } from "@solid-primitives/event-listener"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
@@ -424,9 +425,9 @@ function SettingsKeybindsV2() {
|
||||
filtered={controller.catalog.filtered}
|
||||
title={controller.catalog.title}
|
||||
keybind={controller.catalog.keybind}
|
||||
active={controller.capture.active}
|
||||
active={controller.capture.active()}
|
||||
onCapture={controller.capture.toggle}
|
||||
hasOverrides={controller.settings.hasOverrides}
|
||||
hasOverrides={controller.settings.hasOverrides()}
|
||||
onReset={controller.settings.reset}
|
||||
/>
|
||||
)
|
||||
@@ -437,9 +438,9 @@ function SettingsKeybindsV2View(props: {
|
||||
filtered: (query: string) => Map<KeybindGroup, string[]>
|
||||
title: (id: string) => string
|
||||
keybind: (id: string) => string
|
||||
active: () => string | null
|
||||
active: string | null
|
||||
onCapture: (id: string) => void
|
||||
hasOverrides: () => boolean
|
||||
hasOverrides: boolean
|
||||
onReset: () => void
|
||||
}) {
|
||||
const language = useLanguage()
|
||||
@@ -452,7 +453,7 @@ function SettingsKeybindsV2View(props: {
|
||||
<div class="settings-v2-tab-header settings-v2-tab-header--stacked">
|
||||
<div class="settings-v2-tab-header-row">
|
||||
<h2 class="settings-v2-tab-title">{language.t("settings.shortcuts.title")}</h2>
|
||||
<ButtonV2 variant="ghost" onClick={props.onReset} disabled={!props.hasOverrides()}>
|
||||
<ButtonV2 variant="ghost" onClick={props.onReset} disabled={!props.hasOverrides}>
|
||||
{language.t("settings.shortcuts.reset.button")}
|
||||
</ButtonV2>
|
||||
</div>
|
||||
@@ -498,12 +499,12 @@ function SettingsKeybindsV2View(props: {
|
||||
data-keybind-id={id}
|
||||
classList={{
|
||||
"settings-v2-keybind-button": true,
|
||||
"settings-v2-keybind-button--active": props.active() === id,
|
||||
"settings-v2-keybind-button--active": props.active === id,
|
||||
}}
|
||||
onClick={() => props.onCapture(id)}
|
||||
>
|
||||
<Show
|
||||
when={props.active() === id}
|
||||
when={props.active === id}
|
||||
fallback={props.keybind(id) || language.t("settings.shortcuts.unassigned")}
|
||||
>
|
||||
{language.t("settings.shortcuts.pressKeys")}
|
||||
@@ -674,8 +675,6 @@ export const SettingsKeybinds: Component<{ v2?: boolean }> = (props) => {
|
||||
</Show>
|
||||
)
|
||||
|
||||
const List = props.v2 ? SettingsListV2 : SettingsList
|
||||
|
||||
const groups = (
|
||||
<div
|
||||
classList={{
|
||||
@@ -700,7 +699,7 @@ export const SettingsKeybinds: Component<{ v2?: boolean }> = (props) => {
|
||||
>
|
||||
{language.t(groupKey[group])}
|
||||
</h3>
|
||||
<List>
|
||||
<Dynamic component={props.v2 ? SettingsListV2 : SettingsList}>
|
||||
<For each={filtered().get(group) ?? []}>
|
||||
{(id) => (
|
||||
<div class="flex items-center justify-between gap-4 py-3 border-b border-border-weak-base last:border-none">
|
||||
@@ -735,7 +734,7 @@ export const SettingsKeybinds: Component<{ v2?: boolean }> = (props) => {
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</List>
|
||||
</Dynamic>
|
||||
</div>
|
||||
</Show>
|
||||
)}
|
||||
|
||||
@@ -103,7 +103,7 @@ export const DialogSettings: Component<{
|
||||
<SettingsServersV2 />
|
||||
</TabsV2.Content>
|
||||
<TabsV2.Content value="providers" class="settings-v2-panel">
|
||||
<SettingsProvidersV2 directory={directory} onBack={showProviders} />
|
||||
<SettingsProvidersV2 directory={directory()} onBack={showProviders} />
|
||||
</TabsV2.Content>
|
||||
<TabsV2.Content value="models" class="settings-v2-panel">
|
||||
<SettingsModelsV2 />
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||
import { showToast } from "@/utils/toast"
|
||||
import { popularProviders, useProviders } from "@/hooks/use-providers"
|
||||
import { createMemo, type Accessor, type Component, For, Show } from "solid-js"
|
||||
import { createMemo, type Component, For, Show } from "solid-js"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useServerSDK } from "@/context/server-sdk"
|
||||
import { useServerSync } from "@/context/server-sync"
|
||||
@@ -30,14 +30,14 @@ const PROVIDER_NOTES = [
|
||||
const PROVIDER_ICON_SIZE = 16
|
||||
|
||||
export const SettingsProvidersV2: Component<{
|
||||
directory: Accessor<string | undefined>
|
||||
directory: string | undefined
|
||||
onBack?: () => void
|
||||
}> = (props) => {
|
||||
const dialog = useDialog()
|
||||
const language = useLanguage()
|
||||
const serverSdk = useServerSDK()
|
||||
const serverSync = useServerSync()
|
||||
const providers = useProviders(props.directory)
|
||||
const providers = useProviders(() => props.directory)
|
||||
const providerConnect = useProviderConnectController({ onBack: props.onBack })
|
||||
|
||||
const connect = (provider?: string) => {
|
||||
@@ -116,7 +116,7 @@ export const SettingsProvidersV2: Component<{
|
||||
}
|
||||
|
||||
const disconnect = async (providerID: string, name: string) => {
|
||||
const location = props.directory() ? { directory: props.directory() } : undefined
|
||||
const location = props.directory ? { directory: props.directory } : undefined
|
||||
await serverSdk()
|
||||
.api.integration.get({ integrationID: providerID, location })
|
||||
.then(async (integration) => {
|
||||
|
||||
@@ -1,16 +1,7 @@
|
||||
import { Switch } from "@opencode-ai/ui/switch"
|
||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||
import { showToast } from "@/utils/toast"
|
||||
import {
|
||||
type Accessor,
|
||||
createEffect,
|
||||
createMemo,
|
||||
createResource,
|
||||
For,
|
||||
type JSXElement,
|
||||
onCleanup,
|
||||
Show,
|
||||
} from "solid-js"
|
||||
import { createEffect, createMemo, createResource, For, type JSXElement, onCleanup, Show } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
@@ -109,7 +100,7 @@ type ServerStatusItem = {
|
||||
onSelect: () => void
|
||||
}
|
||||
|
||||
export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
|
||||
export function StatusPopoverBody(props: { shown: boolean }) {
|
||||
const sync = useSync()
|
||||
const sdk = useSDK()
|
||||
const global = useGlobal()
|
||||
@@ -125,10 +116,6 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
|
||||
})
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
if (!props.shown()) return
|
||||
})
|
||||
|
||||
let dialogRun = 0
|
||||
let dialogDead = false
|
||||
onCleanup(() => {
|
||||
@@ -147,7 +134,7 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
|
||||
const lspItems = createMemo(() => sync().data.lsp ?? [])
|
||||
const lspCount = createMemo(() => lspItems().length)
|
||||
const [pluginList] = createResource(
|
||||
() => (props.shown() ? sdk().directory : undefined),
|
||||
() => (props.shown ? sdk().directory : undefined),
|
||||
(directory) =>
|
||||
sdk()
|
||||
.api.plugin.list({ location: { directory } })
|
||||
|
||||
@@ -74,7 +74,7 @@ export function StatusPopover() {
|
||||
<div class="w-[360px] h-14 rounded-xl bg-background-strong shadow-[var(--shadow-lg-border-base)]" />
|
||||
}
|
||||
>
|
||||
<Body shown={shown} />
|
||||
<Body shown={shown()} />
|
||||
</Suspense>
|
||||
</Show>
|
||||
</Popover>
|
||||
@@ -114,7 +114,7 @@ function DirectoryStatusPopover() {
|
||||
onOpenChange: setShown,
|
||||
body: () => (
|
||||
<StatusPopoverBody shown={shown()}>
|
||||
<Body shown={shown} />
|
||||
<Body shown={shown()} />
|
||||
</StatusPopoverBody>
|
||||
),
|
||||
}))
|
||||
|
||||
@@ -175,6 +175,7 @@ export const Terminal = (props: TerminalProps) => {
|
||||
const settings = useSettings()
|
||||
const theme = useTheme()
|
||||
const language = useLanguage()
|
||||
// Intentional mount-time capture: the imperative xterm/WebSocket lifecycle needs stable values, and Terminal remounts when the SDK scope changes.
|
||||
const directory = sdk().directory
|
||||
const url = sdk().url
|
||||
let container!: HTMLDivElement
|
||||
|
||||
@@ -22,14 +22,14 @@ export function TabNavItem(props: {
|
||||
ref?: Ref<HTMLDivElement>
|
||||
href: string
|
||||
server: ServerConnection.Key
|
||||
session: () => SessionInfo | undefined
|
||||
session: SessionInfo | undefined
|
||||
fallbackTitle?: string
|
||||
onRename: (title: string) => Promise<void>
|
||||
onClose: () => void
|
||||
onNavigate: () => void
|
||||
active?: boolean
|
||||
forceTruncate?: boolean
|
||||
suppressNavigation?: () => boolean
|
||||
suppressNavigation?: boolean
|
||||
dragging?: boolean
|
||||
pressed?: boolean
|
||||
hidden?: boolean
|
||||
@@ -52,22 +52,22 @@ export function TabNavItem(props: {
|
||||
if (conn) return global.ensureServerCtx(conn)
|
||||
})
|
||||
const project = createMemo(() => {
|
||||
const session = props.session()
|
||||
const session = props.session
|
||||
if (!session) return
|
||||
return projectForSession(session, serverCtx()?.projects.list() ?? [])
|
||||
})
|
||||
const title = createMemo(() => {
|
||||
const session = props.session()
|
||||
const session = props.session
|
||||
return session ? sessionLabel(session) : props.fallbackTitle
|
||||
})
|
||||
|
||||
const projectName = createMemo(() => {
|
||||
const session = props.session()
|
||||
const session = props.session
|
||||
if (!session) return
|
||||
return displayName(project() ?? { worktree: session.location.directory })
|
||||
})
|
||||
const previewPath = createMemo(() => {
|
||||
const session = props.session()
|
||||
const session = props.session
|
||||
if (!session) return
|
||||
const home = serverCtx()?.sync.data.path.home
|
||||
return home ? session.location.directory.replace(home, "~") : session.location.directory
|
||||
@@ -80,7 +80,7 @@ export function TabNavItem(props: {
|
||||
})
|
||||
|
||||
const [popoverOpen, setPopoverOpen] = createSignal(false)
|
||||
const previewBlocked = () => !!props.dragging || editing() || !!props.pressed || !props.session()
|
||||
const previewBlocked = () => !!props.dragging || editing() || !!props.pressed || !props.session
|
||||
|
||||
const measureTitleOverflow = () => {
|
||||
if (!titleEl || editing()) {
|
||||
@@ -121,7 +121,7 @@ export function TabNavItem(props: {
|
||||
const closeRename = async (save: boolean) => {
|
||||
if (rename.isPending || !editing()) return
|
||||
|
||||
const original = props.session()?.title ?? ""
|
||||
const original = props.session?.title ?? ""
|
||||
const next = (titleEl.textContent ?? "").trim()
|
||||
|
||||
titleEl.scrollLeft = 0
|
||||
@@ -146,7 +146,7 @@ export function TabNavItem(props: {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
if (!canOpenTabRename(props.dragging, editing(), rename.isPending)) return
|
||||
const session = props.session()
|
||||
const session = props.session
|
||||
if (!session) return
|
||||
titleEl.textContent = session.title ?? ""
|
||||
setEditing(true)
|
||||
@@ -213,7 +213,7 @@ export function TabNavItem(props: {
|
||||
// Navigate on mousedown to shave the press-release delay off tab switches.
|
||||
if (event.button !== 0) return
|
||||
if (editing()) return
|
||||
if (props.suppressNavigation?.()) return
|
||||
if (props.suppressNavigation) return
|
||||
props.onNavigate()
|
||||
}}
|
||||
onClick={(event) => {
|
||||
@@ -221,14 +221,14 @@ export function TabNavItem(props: {
|
||||
// Mouse navigation already happened on mousedown; detail 0 means keyboard activation.
|
||||
if (event.detail > 0) return
|
||||
if (editing()) return
|
||||
if (props.suppressNavigation?.()) return
|
||||
if (props.suppressNavigation) return
|
||||
props.onNavigate()
|
||||
}}
|
||||
class="flex h-full min-w-0 flex-1 flex-row items-center gap-1.5 text-[13px] font-medium text-v2-text-text-faint group-data-[active='true']:text-v2-text-text-base group-data-[editing='true']:text-v2-text-text-base [-webkit-user-drag:none]"
|
||||
>
|
||||
<span data-slot="project-avatar-slot" class="flex size-4 shrink-0 items-center justify-center">
|
||||
<Show
|
||||
when={props.session()}
|
||||
when={props.session}
|
||||
keyed
|
||||
fallback={
|
||||
<span class="block size-4 rounded-[3px] border border-v2-border-border-muted" aria-hidden="true" />
|
||||
@@ -267,7 +267,7 @@ export function TabNavItem(props: {
|
||||
}
|
||||
if (event.key !== "Escape") return
|
||||
event.preventDefault()
|
||||
titleEl.textContent = props.session()?.title ?? ""
|
||||
titleEl.textContent = props.session?.title ?? ""
|
||||
void closeRename(false)
|
||||
}}
|
||||
onBlur={() => void closeRename(true)}
|
||||
@@ -308,7 +308,7 @@ export function TabNavItem(props: {
|
||||
}}
|
||||
data={{
|
||||
projectName: projectName(),
|
||||
title: props.session()?.title,
|
||||
title: props.session?.title,
|
||||
path: previewPath(),
|
||||
serverName: serverLabel(),
|
||||
}}
|
||||
@@ -323,7 +323,7 @@ export function DraftTabItem(props: {
|
||||
active?: boolean
|
||||
onNavigate: () => void
|
||||
onClose: () => void
|
||||
suppressNavigation?: () => boolean
|
||||
suppressNavigation?: boolean
|
||||
dragging?: boolean
|
||||
pressed?: boolean
|
||||
hidden?: boolean
|
||||
@@ -366,14 +366,14 @@ export function DraftTabItem(props: {
|
||||
onMouseDown={(event) => {
|
||||
// Navigate on mousedown to shave the press-release delay off tab switches.
|
||||
if (event.button !== 0) return
|
||||
if (props.suppressNavigation?.()) return
|
||||
if (props.suppressNavigation) return
|
||||
props.onNavigate()
|
||||
}}
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
// Mouse navigation already happened on mousedown; detail 0 means keyboard activation.
|
||||
if (event.detail > 0) return
|
||||
if (props.suppressNavigation?.()) return
|
||||
if (props.suppressNavigation) return
|
||||
props.onNavigate()
|
||||
}}
|
||||
class="flex h-full min-w-0 flex-1 flex-row items-center gap-1.5 text-[13px] font-medium text-v2-text-text-faint group-data-[active='true']:text-v2-text-text-base [-webkit-user-drag:none]"
|
||||
|
||||
@@ -24,10 +24,10 @@ import type { SessionInfo } from "@opencode-ai/client/promise"
|
||||
function SessionTabSlot(props: {
|
||||
tab: SessionTab
|
||||
id: string
|
||||
index: () => number
|
||||
active: () => boolean
|
||||
index: number
|
||||
active: boolean
|
||||
forceTruncate: boolean
|
||||
session: () => SessionInfo | undefined
|
||||
session: SessionInfo | undefined
|
||||
fallbackTitle?: string
|
||||
onRename: (title: string) => Promise<void>
|
||||
onNavigate: (element: HTMLDivElement) => void
|
||||
@@ -38,7 +38,7 @@ function SessionTabSlot(props: {
|
||||
return props.id
|
||||
},
|
||||
get index() {
|
||||
return props.index()
|
||||
return props.index
|
||||
},
|
||||
})
|
||||
let ref!: HTMLDivElement
|
||||
@@ -48,7 +48,7 @@ function SessionTabSlot(props: {
|
||||
ref={sortable.ref}
|
||||
data-titlebar-tab-slot
|
||||
data-tab-key={props.id}
|
||||
data-active={props.active()}
|
||||
data-active={props.active}
|
||||
class="relative flex w-56 min-w-7 max-w-56 flex-shrink"
|
||||
>
|
||||
<TabNavItem
|
||||
@@ -62,7 +62,7 @@ function SessionTabSlot(props: {
|
||||
onRename={props.onRename}
|
||||
onNavigate={() => props.onNavigate(ref)}
|
||||
onClose={props.onClose}
|
||||
active={props.active()}
|
||||
active={props.active}
|
||||
forceTruncate={props.forceTruncate}
|
||||
dragging={sortable.isDragSource()}
|
||||
/>
|
||||
@@ -73,34 +73,34 @@ function SessionTabSlot(props: {
|
||||
function SessionTabEntry(props: {
|
||||
tab: SessionTab
|
||||
id: string
|
||||
index: () => number
|
||||
active: () => boolean
|
||||
index: number
|
||||
active: boolean
|
||||
forceTruncate: boolean
|
||||
serverCtx: () => ServerCtx | undefined
|
||||
serverCtx: ServerCtx | undefined
|
||||
onVisibleChange: (visible: boolean) => void
|
||||
onNavigate: (element: HTMLDivElement) => void
|
||||
onClose: () => void
|
||||
}) {
|
||||
const tabs = useTabs()
|
||||
const language = useLanguage()
|
||||
const sdk = createMemo(() => props.serverCtx()?.sdk ?? null)
|
||||
const cachedSession = createMemo(() => props.serverCtx()?.sync.session.peek(props.tab.sessionId))
|
||||
const sdk = createMemo(() => props.serverCtx?.sdk ?? null)
|
||||
const cachedSession = createMemo(() => props.serverCtx?.sync.session.peek(props.tab.sessionId))
|
||||
const persisted = createMemo(() => tabs.info[props.id])
|
||||
const [loadedSession] = createResource(
|
||||
() => {
|
||||
const ctx = props.serverCtx()
|
||||
const ctx = props.serverCtx
|
||||
return ctx ? { id: props.tab.sessionId, ctx } : null
|
||||
},
|
||||
({ id, ctx }) => ctx.sync.session.resolve(id).catch(() => undefined),
|
||||
)
|
||||
const session = createMemo(() => cachedSession() ?? loadedSession())
|
||||
const missingSession = createMemo(() => !!props.serverCtx() && !loadedSession.loading && !session())
|
||||
const missingSession = createMemo(() => !!props.serverCtx && !loadedSession.loading && !session())
|
||||
const visible = createMemo(() => !!session() || missingSession() || !!persisted()?.title)
|
||||
let prefetched = false
|
||||
|
||||
const rename = async (title: string) => {
|
||||
const value = session()
|
||||
const ctx = props.serverCtx()
|
||||
const ctx = props.serverCtx
|
||||
if (!value || !ctx) return
|
||||
|
||||
ctx.sync.session.remember({ ...value, title })
|
||||
@@ -108,7 +108,7 @@ function SessionTabEntry(props: {
|
||||
await ctx.sdk.api.session.rename({ sessionID: value.id, title })
|
||||
} catch (err) {
|
||||
const current = session()
|
||||
const currentCtx = props.serverCtx()
|
||||
const currentCtx = props.serverCtx
|
||||
if (current && currentCtx) currentCtx.sync.session.remember({ ...current, title: value.title })
|
||||
showToast({
|
||||
title: language.t("common.requestFailed"),
|
||||
@@ -120,7 +120,7 @@ function SessionTabEntry(props: {
|
||||
createEffect(() => props.onVisibleChange(visible()))
|
||||
|
||||
createEffect(() => {
|
||||
const ctx = props.serverCtx()
|
||||
const ctx = props.serverCtx
|
||||
const value = session()
|
||||
if (!ctx || !value || prefetched) return
|
||||
prefetched = true
|
||||
@@ -157,7 +157,7 @@ function SessionTabEntry(props: {
|
||||
index={props.index}
|
||||
active={props.active}
|
||||
forceTruncate={props.forceTruncate}
|
||||
session={session}
|
||||
session={session()}
|
||||
fallbackTitle={persisted()?.title ?? (missingSession() ? language.t("session.tab.unknown") : undefined)}
|
||||
onRename={rename}
|
||||
onNavigate={props.onNavigate}
|
||||
@@ -170,8 +170,8 @@ function SessionTabEntry(props: {
|
||||
function DraftTabSlot(props: {
|
||||
tab: Extract<Tab, { type: "draft" }>
|
||||
id: string
|
||||
index: () => number
|
||||
active: () => boolean
|
||||
index: number
|
||||
active: boolean
|
||||
title: string
|
||||
onNavigate: (element: HTMLDivElement) => void
|
||||
onClose: () => void
|
||||
@@ -181,7 +181,7 @@ function DraftTabSlot(props: {
|
||||
return props.id
|
||||
},
|
||||
get index() {
|
||||
return props.index()
|
||||
return props.index
|
||||
},
|
||||
})
|
||||
let ref!: HTMLDivElement
|
||||
@@ -191,7 +191,7 @@ function DraftTabSlot(props: {
|
||||
ref={sortable.ref}
|
||||
data-titlebar-tab-slot
|
||||
data-tab-key={props.id}
|
||||
data-active={props.active()}
|
||||
data-active={props.active}
|
||||
class="relative flex w-56 min-w-7 max-w-56 flex-shrink"
|
||||
>
|
||||
<DraftTabItem
|
||||
@@ -202,7 +202,7 @@ function DraftTabSlot(props: {
|
||||
title={props.title}
|
||||
onNavigate={() => props.onNavigate(ref)}
|
||||
onClose={props.onClose}
|
||||
active={props.active()}
|
||||
active={props.active}
|
||||
dragging={sortable.isDragSource()}
|
||||
/>
|
||||
</div>
|
||||
@@ -211,7 +211,7 @@ function DraftTabSlot(props: {
|
||||
|
||||
export function TitlebarTabStrip(props: {
|
||||
tabs: Tab[]
|
||||
currentTab: () => Tab | undefined
|
||||
currentTab: Tab | undefined
|
||||
forceTruncate: boolean
|
||||
onNavigate: (tab: Tab, el?: HTMLDivElement) => void
|
||||
onClose: (tab: Tab) => void
|
||||
@@ -248,7 +248,7 @@ export function TitlebarTabStrip(props: {
|
||||
])
|
||||
|
||||
function selectAdjacentTab(offset: -1 | 1) {
|
||||
const current = props.currentTab()
|
||||
const current = props.currentTab
|
||||
const key = adjacentTabKey(visibleTabIds(), current ? tabKey(current) : undefined, offset)
|
||||
const next = props.tabs.find((tab) => tabKey(tab) === key)
|
||||
if (next) props.onNavigate(next)
|
||||
@@ -350,10 +350,10 @@ export function TitlebarTabStrip(props: {
|
||||
<SessionTabEntry
|
||||
tab={tab}
|
||||
id={id}
|
||||
index={visibleIndex}
|
||||
active={() => props.currentTab() === tab}
|
||||
index={visibleIndex()}
|
||||
active={props.currentTab === tab}
|
||||
forceTruncate={props.forceTruncate}
|
||||
serverCtx={serverCtx}
|
||||
serverCtx={serverCtx()}
|
||||
onVisibleChange={(visible) => setVisibility(id, visible)}
|
||||
onNavigate={(element) => {
|
||||
ref = element
|
||||
@@ -368,8 +368,8 @@ export function TitlebarTabStrip(props: {
|
||||
<DraftTabSlot
|
||||
tab={tab}
|
||||
id={id}
|
||||
index={visibleIndex}
|
||||
active={() => props.currentTab() === tab}
|
||||
index={visibleIndex()}
|
||||
active={props.currentTab === tab}
|
||||
title={language.t("command.session.new")}
|
||||
onNavigate={(element) => {
|
||||
ref = element
|
||||
|
||||
@@ -46,8 +46,8 @@ const windowsControlsBaseWidth = 138 // 3 native Windows caption buttons at 46px
|
||||
const macTrafficLightsBaseWidth = 84
|
||||
|
||||
export type TitlebarUpdate = {
|
||||
version: () => string | undefined
|
||||
installing: () => boolean
|
||||
version: string | undefined
|
||||
installing: boolean
|
||||
install: () => void
|
||||
}
|
||||
|
||||
@@ -121,8 +121,8 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl
|
||||
const hasProjects = createMemo(() => layout.projects.list().length > 0)
|
||||
const nav = createMemo(() => (useV2Titlebar() ? settings.general.showNavigation() : true))
|
||||
const updateState = createMemo<TitlebarUpdatePillState>(() => {
|
||||
const installing = props.update?.installing() ?? false
|
||||
const version = props.update?.version()
|
||||
const installing = props.update?.installing ?? false
|
||||
const version = props.update?.version
|
||||
return {
|
||||
visible: version !== undefined || installing,
|
||||
installing,
|
||||
@@ -392,7 +392,7 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl
|
||||
|
||||
<TitlebarTabStrip
|
||||
tabs={tabsStore}
|
||||
currentTab={currentTab}
|
||||
currentTab={currentTab()}
|
||||
forceTruncate={tabsAreOverflowing()}
|
||||
onOverflowChange={setTabsAreOverflowing}
|
||||
onNavigate={(tab, el) => {
|
||||
@@ -657,12 +657,10 @@ function ChannelIndicator(props: { debugTools?: { visible: boolean; toggle: () =
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{["local", "beta", "dev"].includes(channel) && (
|
||||
<div class="bg-icon-interactive-base text-[#FFF] font-medium px-2 rounded-sm uppercase font-mono">
|
||||
{channel.toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
<Show when={["local", "beta", "dev"].includes(channel)}>
|
||||
<div class="bg-icon-interactive-base text-[#FFF] font-medium px-2 rounded-sm uppercase font-mono">
|
||||
{channel.toUpperCase()}
|
||||
</div>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Show, type JSX } from "solid-js"
|
||||
import { For, Show, type JSX } from "solid-js"
|
||||
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
@@ -50,7 +50,20 @@ export function WindowsAppMenu(props: {
|
||||
|
||||
return (
|
||||
<DropdownMenu gutter={4} modal={false} placement="bottom-start">
|
||||
{props.variant === "v2" ? (
|
||||
<Show
|
||||
when={props.variant === "v2"}
|
||||
fallback={
|
||||
<DropdownMenu.Trigger
|
||||
as={IconButton}
|
||||
icon="menu"
|
||||
variant="ghost"
|
||||
class="titlebar-icon rounded-md shrink-0"
|
||||
aria-label={language.t("desktop.menu.ariaLabel")}
|
||||
onPointerDown={rememberFocus}
|
||||
onKeyDown={rememberFocus}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div
|
||||
data-component="desktop-icon-button"
|
||||
class="flex h-7 w-9 shrink-0 items-center justify-center rounded-[6px] px-1"
|
||||
@@ -65,39 +78,31 @@ export function WindowsAppMenu(props: {
|
||||
onKeyDown={rememberFocus}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<DropdownMenu.Trigger
|
||||
as={IconButton}
|
||||
icon="menu"
|
||||
variant="ghost"
|
||||
class="titlebar-icon rounded-md shrink-0"
|
||||
aria-label={language.t("desktop.menu.ariaLabel")}
|
||||
onPointerDown={rememberFocus}
|
||||
onKeyDown={rememberFocus}
|
||||
/>
|
||||
)}
|
||||
</Show>
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content class="desktop-app-menu">
|
||||
<DropdownMenu.Group>
|
||||
<DropdownMenu.GroupLabel class="desktop-app-menu-heading">OpenCode</DropdownMenu.GroupLabel>
|
||||
{DESKTOP_MENU.filter((menu) => desktopMenuVisible(menu, "windows")).map((menu) => (
|
||||
<DesktopMenuSubmenu label={language.t(menu.labelKey)}>
|
||||
{menu.items
|
||||
?.filter((entry) => desktopMenuVisible(entry, "windows"))
|
||||
.map((entry) =>
|
||||
entry.type === "separator" ? (
|
||||
<DropdownMenu.Separator />
|
||||
) : (
|
||||
<DesktopMenuItem
|
||||
label={entry.labelKey ? language.t(entry.labelKey) : ""}
|
||||
keybind={entry.command ? props.command.keybind(entry.command) : entry.accelerator?.windows}
|
||||
disabled={entry.command ? commandDisabled(entry.command) : false}
|
||||
onSelect={() => runEntry(entry)}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</DesktopMenuSubmenu>
|
||||
))}
|
||||
<For each={DESKTOP_MENU.filter((menu) => desktopMenuVisible(menu, "windows"))}>
|
||||
{(menu) => (
|
||||
<DesktopMenuSubmenu label={language.t(menu.labelKey)}>
|
||||
<For each={menu.items?.filter((entry) => desktopMenuVisible(entry, "windows"))}>
|
||||
{(entry) => {
|
||||
// Static menu data: an early return keeps the union narrowing a Show fallback would lose.
|
||||
if (entry.type === "separator") return <DropdownMenu.Separator />
|
||||
return (
|
||||
<DesktopMenuItem
|
||||
label={entry.labelKey ? language.t(entry.labelKey) : ""}
|
||||
keybind={entry.command ? props.command.keybind(entry.command) : entry.accelerator?.windows}
|
||||
disabled={entry.command ? commandDisabled(entry.command) : false}
|
||||
onSelect={() => runEntry(entry)}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</DesktopMenuSubmenu>
|
||||
)}
|
||||
</For>
|
||||
</DropdownMenu.Group>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Binary } from "@opencode-ai/core/util/binary"
|
||||
import { ProjectDirectories } from "@opencode-ai/schema/project-directories"
|
||||
import { produce, reconcile, type SetStoreFunction, type Store } from "solid-js/store"
|
||||
import type { Message, Part, Project, Todo } from "@/types"
|
||||
import type {
|
||||
@@ -187,6 +188,18 @@ export function applyDirectoryEvent(input: {
|
||||
input.setStore("sessionTotal", (value) => Math.max(0, value - 1))
|
||||
break
|
||||
}
|
||||
case "project.directory.resolved": {
|
||||
const properties = event.properties as { projectID: string; directory: string; previous: string }
|
||||
input.store.session.forEach((session, index) => {
|
||||
const adopted = ProjectDirectories.adopt(
|
||||
{ projectID: session.projectID, directory: session.location.directory },
|
||||
properties,
|
||||
)
|
||||
if (!adopted) return
|
||||
input.setStore("session", index, (current) => ({ ...current, ...adopted }))
|
||||
})
|
||||
break
|
||||
}
|
||||
case "session.renamed": {
|
||||
const properties = event.properties as { sessionID: string; title: string }
|
||||
const result = Binary.search(input.store.session, properties.sessionID, (session) => session.id)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type Accessor, createMemo, createResource } from "solid-js"
|
||||
import { createMemo, createResource } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { DateTime } from "luxon"
|
||||
import { filter, firstBy, flat, groupBy, mapValues, pipe, uniqueBy, values } from "remeda"
|
||||
@@ -25,8 +25,8 @@ function modelKey(model: ModelKey) {
|
||||
export const { use: useModels, provider: ModelsProvider } = createSimpleContext({
|
||||
name: "Models",
|
||||
gate: false,
|
||||
init: (props: { directory?: Accessor<string | undefined> } = {}) => {
|
||||
const providers = useProviders(() => props.directory?.())
|
||||
init: (props: { directory?: string } = {}) => {
|
||||
const providers = useProviders(() => props.directory)
|
||||
|
||||
const [store, setStore, _, ready] = persisted(
|
||||
Persist.global("model", ["model.v1"]),
|
||||
|
||||
@@ -6,12 +6,9 @@ export type { DirectorySDK } from "./server-sdk"
|
||||
const context = createSimpleContext({
|
||||
name: "SDK",
|
||||
// Resolves the directory-scoped SDK reactively from the (possibly changing) server.
|
||||
init: (props: { directory: string | Accessor<string> }) => {
|
||||
init: (props: { directory: string }) => {
|
||||
const serverSDK = useServerSDK()
|
||||
return createMemo(() => {
|
||||
const directory = typeof props.directory === "function" ? props.directory() : props.directory
|
||||
return serverSDK().ensureDirSdkContext(directory)
|
||||
})
|
||||
return createMemo(() => serverSDK().ensureDirSdkContext(props.directory))
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Event } from "@/types"
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import { createGlobalEmitter } from "@solid-primitives/event-bus"
|
||||
import { makeEventListener } from "@solid-primitives/event-listener"
|
||||
import { type Accessor, batch, createMemo, onCleanup, onMount } from "solid-js"
|
||||
import { batch, createMemo, onCleanup, onMount } from "solid-js"
|
||||
import { createApiForServer, type ServerApi } from "@/utils/server"
|
||||
import { useLanguage } from "./language"
|
||||
import { usePlatform } from "./platform"
|
||||
@@ -270,13 +270,13 @@ export const { use: useServerSDK, provider: ServerSDKProvider } = createSimpleCo
|
||||
name: "ServerSDK",
|
||||
// Returns an accessor so the resolved server can change reactively (e.g. a
|
||||
// /new-session draft retargeting its server) without re-instantiating the subtree.
|
||||
init: (props: { server?: Accessor<ServerConnection.Any | undefined> }) => {
|
||||
init: (props: { server?: ServerConnection.Any }) => {
|
||||
const global = useGlobal()
|
||||
const language = useLanguage()
|
||||
const server = useServer()
|
||||
|
||||
return createMemo<ServerSDK>(() => {
|
||||
const conn = props.server?.() ?? server.current
|
||||
const conn = props.server ?? server.current
|
||||
if (!conn) throw new Error(language.t("error.serverSDK.noServerAvailable"))
|
||||
return global.ensureServerCtx(conn).sdk
|
||||
})
|
||||
|
||||
@@ -18,18 +18,18 @@ describe("v2 session reducer", () => {
|
||||
apply({
|
||||
...base,
|
||||
id: "evt_admitted",
|
||||
type: "session.input.admitted",
|
||||
type: "session.inbox.enqueued",
|
||||
data: {
|
||||
sessionID: "ses_1",
|
||||
inputID: "msg_user",
|
||||
input: { type: "user", delivery: "steer", data: { text: "hello" } },
|
||||
inboxID: "msg_user",
|
||||
item: { type: "user", delivery: "steer", payload: { text: "hello" } },
|
||||
},
|
||||
})
|
||||
apply({
|
||||
...base,
|
||||
id: "evt_promoted",
|
||||
type: "session.input.promoted",
|
||||
data: { sessionID: "ses_1", inputID: "msg_user" },
|
||||
type: "session.inbox.delivered",
|
||||
data: { sessionID: "ses_1", inboxID: "msg_user" },
|
||||
})
|
||||
apply({
|
||||
...base,
|
||||
@@ -203,8 +203,8 @@ describe("v2 session reducer", () => {
|
||||
event({
|
||||
...base,
|
||||
id: "evt_promoted",
|
||||
type: "session.input.promoted",
|
||||
data: { sessionID: "ses_1", inputID: "msg_user" },
|
||||
type: "session.inbox.delivered",
|
||||
data: { sessionID: "ses_1", inboxID: "msg_user" },
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -218,11 +218,11 @@ describe("v2 session reducer", () => {
|
||||
event({
|
||||
...base,
|
||||
id: "evt_admitted",
|
||||
type: "session.input.admitted",
|
||||
type: "session.inbox.enqueued",
|
||||
data: {
|
||||
sessionID: "ses_1",
|
||||
inputID: "msg_user",
|
||||
input: { type: "user", delivery: "queue", data: { text: "cancel me" } },
|
||||
inboxID: "msg_user",
|
||||
item: { type: "user", delivery: "queue", payload: { text: "cancel me" } },
|
||||
},
|
||||
}),
|
||||
)
|
||||
@@ -231,8 +231,8 @@ describe("v2 session reducer", () => {
|
||||
event({
|
||||
...base,
|
||||
id: "evt_cancelled",
|
||||
type: "session.input.cancelled",
|
||||
data: { sessionID: "ses_1", inputID: "msg_user" },
|
||||
type: "session.inbox.cancelled",
|
||||
data: { sessionID: "ses_1", inboxID: "msg_user" },
|
||||
}),
|
||||
)
|
||||
const result = reducer.reduce(
|
||||
@@ -240,8 +240,8 @@ describe("v2 session reducer", () => {
|
||||
event({
|
||||
...base,
|
||||
id: "evt_promoted",
|
||||
type: "session.input.promoted",
|
||||
data: { sessionID: "ses_1", inputID: "msg_user" },
|
||||
type: "session.inbox.delivered",
|
||||
data: { sessionID: "ses_1", inboxID: "msg_user" },
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -255,11 +255,11 @@ describe("v2 session reducer", () => {
|
||||
event({
|
||||
...base,
|
||||
id: "evt_admitted",
|
||||
type: "session.input.admitted",
|
||||
type: "session.inbox.enqueued",
|
||||
data: {
|
||||
sessionID: "ses_1",
|
||||
inputID: "msg_user",
|
||||
input: { type: "user", delivery: "queue", data: { text: "steer me" } },
|
||||
inboxID: "msg_user",
|
||||
item: { type: "user", delivery: "queue", payload: { text: "steer me" } },
|
||||
},
|
||||
}),
|
||||
)
|
||||
@@ -268,8 +268,8 @@ describe("v2 session reducer", () => {
|
||||
event({
|
||||
...base,
|
||||
id: "evt_steered",
|
||||
type: "session.input.steered",
|
||||
data: { sessionID: "ses_1", inputID: "msg_user" },
|
||||
type: "session.inbox.delivery.changed",
|
||||
data: { sessionID: "ses_1", inboxID: "msg_user", delivery: "steer" },
|
||||
}),
|
||||
)
|
||||
reducer.reduce(
|
||||
@@ -277,8 +277,8 @@ describe("v2 session reducer", () => {
|
||||
event({
|
||||
...base,
|
||||
id: "evt_queued",
|
||||
type: "session.input.queued",
|
||||
data: { sessionID: "ses_1", inputID: "msg_user" },
|
||||
type: "session.inbox.delivery.changed",
|
||||
data: { sessionID: "ses_1", inboxID: "msg_user", delivery: "queue" },
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -287,8 +287,8 @@ describe("v2 session reducer", () => {
|
||||
event({
|
||||
...base,
|
||||
id: "evt_promoted",
|
||||
type: "session.input.promoted",
|
||||
data: { sessionID: "ses_1", inputID: "msg_user" },
|
||||
type: "session.inbox.delivered",
|
||||
data: { sessionID: "ses_1", inboxID: "msg_user" },
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { OpenCodeEvent, SessionMessageInfo, SessionPendingMessage } from "@opencode-ai/client/promise"
|
||||
import type { OpenCodeEvent, SessionInboxItem, SessionMessageInfo } from "@opencode-ai/client/promise"
|
||||
|
||||
type Assistant = Extract<SessionMessageInfo, { type: "assistant" }>
|
||||
type Compaction = Extract<SessionMessageInfo, { type: "compaction" }>
|
||||
@@ -12,7 +12,7 @@ export type V2SessionReduction = {
|
||||
}
|
||||
|
||||
export function createV2SessionReducer() {
|
||||
const pending = new Map<string, SessionPendingMessage>()
|
||||
const pending = new Map<string, SessionInboxItem>()
|
||||
|
||||
const reduce = (source: readonly SessionMessageInfo[], event: OpenCodeEvent): V2SessionReduction | undefined => {
|
||||
if (!("data" in event) || !("sessionID" in event.data) || typeof event.data.sessionID !== "string") return
|
||||
@@ -26,32 +26,33 @@ export function createV2SessionReducer() {
|
||||
result(source.some((item) => item.id === message.id) ? [...source] : [...source, message], [message.id])
|
||||
|
||||
switch (event.type) {
|
||||
case "session.input.admitted":
|
||||
pending.set(key(sessionID, event.data.inputID), event.data.input)
|
||||
case "session.inbox.enqueued":
|
||||
pending.set(key(sessionID, event.data.inboxID), event.data.item)
|
||||
return result([...source])
|
||||
case "session.input.cancelled":
|
||||
pending.delete(key(sessionID, event.data.inputID))
|
||||
case "session.inbox.cancelled":
|
||||
pending.delete(key(sessionID, event.data.inboxID))
|
||||
return
|
||||
case "session.input.promoted": {
|
||||
const input = pending.get(key(sessionID, event.data.inputID))
|
||||
pending.delete(key(sessionID, event.data.inputID))
|
||||
if (!input) return { ...result([...source]), missing: event.data.inputID }
|
||||
case "session.inbox.delivered": {
|
||||
const input = pending.get(key(sessionID, event.data.inboxID))
|
||||
pending.delete(key(sessionID, event.data.inboxID))
|
||||
if (!input) return { ...result([...source]), missing: event.data.inboxID }
|
||||
if (input.type === "user")
|
||||
return append({
|
||||
id: event.data.inputID,
|
||||
id: event.data.inboxID,
|
||||
type: "user",
|
||||
metadata: input.data.metadata,
|
||||
text: input.data.text,
|
||||
files: input.data.files,
|
||||
agents: input.data.agents,
|
||||
metadata: input.payload.metadata,
|
||||
text: input.payload.text,
|
||||
files: input.payload.files,
|
||||
agents: input.payload.agents,
|
||||
time: { created: event.created },
|
||||
})
|
||||
if (input.type !== "synthetic") return result([...source])
|
||||
return append({
|
||||
id: event.data.inputID,
|
||||
id: event.data.inboxID,
|
||||
type: "synthetic",
|
||||
metadata: input.data.metadata,
|
||||
text: input.data.text,
|
||||
description: input.data.description,
|
||||
metadata: input.payload.metadata,
|
||||
text: input.payload.text,
|
||||
description: input.payload.description,
|
||||
time: { created: event.created },
|
||||
})
|
||||
}
|
||||
@@ -351,7 +352,7 @@ export function createV2SessionReducer() {
|
||||
metadata: event.metadata,
|
||||
reason: event.data.reason,
|
||||
summary: "",
|
||||
recent: event.data.recent,
|
||||
recent: event.data.recent ?? "",
|
||||
time: { created: event.created },
|
||||
})
|
||||
case "session.compaction.delta":
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Binary } from "@opencode-ai/core/util/binary"
|
||||
import { ProjectDirectories } from "@opencode-ai/schema/project-directories"
|
||||
import { retry } from "@opencode-ai/core/util/retry"
|
||||
import type { OpenCodeEvent, SessionApi, SessionInfo, SessionMessageInfo } from "@opencode-ai/client/promise"
|
||||
import type { Message, Part, Todo } from "@/types"
|
||||
@@ -894,6 +895,17 @@ export function createServerSession(
|
||||
}
|
||||
|
||||
const applyV2 = (event: OpenCodeEvent) => {
|
||||
if (event.type === "project.directory.resolved") {
|
||||
Object.values(data.info).forEach((info) => {
|
||||
if (!info) return
|
||||
const adopted = ProjectDirectories.adopt(
|
||||
{ projectID: info.projectID, directory: info.location.directory },
|
||||
event.data,
|
||||
)
|
||||
if (adopted) remember({ ...info, ...adopted })
|
||||
})
|
||||
return
|
||||
}
|
||||
if (!("data" in event) || !("sessionID" in event.data) || typeof event.data.sessionID !== "string") return
|
||||
const sessionID = event.data.sessionID
|
||||
const reduction = v2.reduce(data.session_message[sessionID] ?? [], event)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Config, Path, Project, ProviderAuthResponse } from "@/types"
|
||||
import { showToast } from "@/utils/toast"
|
||||
import { getFilename } from "@opencode-ai/core/util/path"
|
||||
import { type Accessor, batch, createMemo, getOwner, onCleanup, onMount, untrack } from "solid-js"
|
||||
import { batch, createMemo, getOwner, onCleanup, onMount, untrack } from "solid-js"
|
||||
import { createStore, produce, reconcile } from "solid-js/store"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import type { InitError } from "../pages/error"
|
||||
@@ -699,13 +699,13 @@ export const { use: useServerSync, provider: ServerSyncProvider } = createSimple
|
||||
name: "ServerSync",
|
||||
// Returns an accessor so the resolved server can change reactively without
|
||||
// re-instantiating the subtree (mirrors useServerSDK).
|
||||
init: (props: { server?: Accessor<ServerConnection.Any | undefined> }) => {
|
||||
init: (props: { server?: ServerConnection.Any }) => {
|
||||
const global = useGlobal()
|
||||
const language = useLanguage()
|
||||
const server = useServer()
|
||||
|
||||
return createMemo<ServerSync>(() => {
|
||||
const conn = props.server?.() ?? server.current
|
||||
const conn = props.server ?? server.current
|
||||
if (!conn) throw new Error(language.t("error.serverSDK.noServerAvailable"))
|
||||
return global.ensureServerCtx(conn).sync
|
||||
})
|
||||
|
||||
@@ -2,7 +2,7 @@ import { DataProvider } from "@opencode-ai/session-ui/context"
|
||||
import { showToast } from "@/utils/toast"
|
||||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||
import { useLocation, useNavigate, useParams } from "@solidjs/router"
|
||||
import { type Accessor, createEffect, createMemo, createResource, onCleanup, type ParentProps, Show } from "solid-js"
|
||||
import { createEffect, createMemo, createResource, onCleanup, type ParentProps, Show } from "solid-js"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { LocalProvider } from "@/context/local"
|
||||
import { SDKProvider } from "@/context/sdk"
|
||||
@@ -15,9 +15,9 @@ import { useServerSync } from "@/context/server-sync"
|
||||
|
||||
export function DirectoryDataProvider(
|
||||
props: ParentProps<{
|
||||
directory: string | Accessor<string>
|
||||
directory: string
|
||||
draftID?: string
|
||||
server?: Accessor<ServerConnection.Key | undefined>
|
||||
server?: ServerConnection.Key
|
||||
}>,
|
||||
) {
|
||||
const location = useLocation()
|
||||
@@ -25,17 +25,16 @@ export function DirectoryDataProvider(
|
||||
const params = useParams()
|
||||
const sync = useSync()
|
||||
const serverSync = useServerSync()
|
||||
const directory = () => (typeof props.directory === "function" ? props.directory() : props.directory)
|
||||
const directory = () => props.directory
|
||||
const slug = createMemo(() => base64Encode(directory()))
|
||||
const href = (sessionID: string) => {
|
||||
const server = props.server?.()
|
||||
if (server) return sessionHref(server, sessionID)
|
||||
if (props.server) return sessionHref(props.server, sessionID)
|
||||
return `/${slug()}/session/${sessionID}`
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
// A draft lives at /new-session?draftId=… and has no directory segment to normalize.
|
||||
if (props.draftID || props.server?.()) return
|
||||
if (props.draftID || props.server) return
|
||||
const next = sync().data.path.directory
|
||||
if (!next || next === directory()) return
|
||||
const path = location.pathname.slice(slug().length + 1)
|
||||
|
||||
@@ -23,8 +23,8 @@ export function Home() {
|
||||
>
|
||||
<ScrollView
|
||||
class="h-full [container-type:size]"
|
||||
thumbContainer={scroll.viewport.thumbTrack}
|
||||
thumbHoverTarget={scroll.viewport.hoverTarget}
|
||||
thumbContainer={scroll.viewport.thumbTrack()}
|
||||
thumbHoverTarget={scroll.viewport.hoverTarget()}
|
||||
viewportRef={scroll.viewport.setViewport}
|
||||
onScroll={(event) => scroll.viewport.update(event.currentTarget.scrollTop)}
|
||||
onWheel={scroll.viewport.containOuterWheel}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type Accessor, createMemo, For, type JSX, onCleanup, Show, splitProps } from "solid-js"
|
||||
import { createMemo, For, type JSX, onCleanup, Show, splitProps } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { DragDropProvider, PointerSensor } from "@dnd-kit/solid"
|
||||
import { isSortable, useSortable } from "@dnd-kit/solid/sortable"
|
||||
@@ -29,16 +29,16 @@ const projectContextMenuID = (server: ServerConnection.Any, directory: string) =
|
||||
|
||||
export type HomeProjectsViewProps = {
|
||||
language: ReturnType<typeof useLanguage>
|
||||
servers: Accessor<ServerConnection.Any[]>
|
||||
projects: Accessor<LocalProject[]>
|
||||
recentlyClosed: Accessor<LocalProject[]>
|
||||
selection: Accessor<HomeProjectSelection>
|
||||
homedir: Accessor<string>
|
||||
servers: ServerConnection.Any[]
|
||||
projects: LocalProject[]
|
||||
recentlyClosed: LocalProject[]
|
||||
selection: HomeProjectSelection
|
||||
homedir: string
|
||||
serverHealth: (server: ServerConnection.Any) => ServerHealth | undefined
|
||||
projectsForServer: (server: ServerConnection.Any) => LocalProject[]
|
||||
collapsed: (server: ServerConnection.Any) => boolean
|
||||
canDefaultServer: Accessor<boolean>
|
||||
defaultServerKey: Accessor<ServerConnection.Key | null | undefined>
|
||||
canDefaultServer: boolean
|
||||
defaultServerKey: ServerConnection.Key | null | undefined
|
||||
canRevealProject: (server: ServerConnection.Any) => boolean
|
||||
unseenCount: (server: ServerConnection.Any, project: LocalProject) => number
|
||||
onWheel: (event: WheelEvent) => void
|
||||
@@ -81,9 +81,7 @@ export function HomeProjectsView(props: HomeProjectsViewProps) {
|
||||
>
|
||||
<div class="flex h-7 min-w-0 shrink-0 items-center justify-between pl-1.5 pr-3">
|
||||
<div class="text-v2-text-text-muted [font-weight:530]">{props.language.t("home.projects")}</div>
|
||||
<Show
|
||||
when={props.servers().length === 1 && !(props.projects().length === 0 && props.recentlyClosed().length > 0)}
|
||||
>
|
||||
<Show when={props.servers.length === 1 && !(props.projects.length === 0 && props.recentlyClosed.length > 0)}>
|
||||
<TooltipV2 placement="bottom" value={props.language.t("home.project.add")}>
|
||||
<IconButtonV2
|
||||
data-action="home-add-project"
|
||||
@@ -91,8 +89,8 @@ export function HomeProjectsView(props: HomeProjectsViewProps) {
|
||||
size="large"
|
||||
class="titlebar-icon [&_[data-slot=icon-svg]]:text-v2-icon-icon-muted"
|
||||
icon={<IconV2 name="folder-add-left" />}
|
||||
disabled={props.serverHealth(props.servers()[0])?.healthy === false}
|
||||
onClick={() => props.onChooseProject(props.servers()[0])}
|
||||
disabled={props.serverHealth(props.servers[0])?.healthy === false}
|
||||
onClick={() => props.onChooseProject(props.servers[0])}
|
||||
aria-label={props.language.t("home.project.add")}
|
||||
/>
|
||||
</TooltipV2>
|
||||
@@ -100,25 +98,20 @@ export function HomeProjectsView(props: HomeProjectsViewProps) {
|
||||
</div>
|
||||
<ScrollView data-slot="home-projects-scroll" class="min-h-0 min-w-0 shrink">
|
||||
<Show
|
||||
when={props.servers().length > 1}
|
||||
when={props.servers.length > 1}
|
||||
fallback={
|
||||
<div class="pr-3">
|
||||
<Show
|
||||
when={props.projects().length > 0}
|
||||
fallback={<HomeProjectEmpty {...props} server={props.servers()[0]} items={props.recentlyClosed()} />}
|
||||
when={props.projects.length > 0}
|
||||
fallback={<HomeProjectEmpty {...props} server={props.servers[0]} items={props.recentlyClosed} />}
|
||||
>
|
||||
<HomeProjectList
|
||||
{...props}
|
||||
{...contextMenuProps}
|
||||
server={props.servers()[0]}
|
||||
items={props.projects()}
|
||||
/>
|
||||
<HomeProjectList {...props} {...contextMenuProps} server={props.servers[0]} items={props.projects} />
|
||||
</Show>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div class="flex min-w-0 flex-col gap-4 pr-3">
|
||||
<For each={props.servers()}>
|
||||
<For each={props.servers}>
|
||||
{(item) => {
|
||||
const projects = () => props.projectsForServer(item)
|
||||
const healthy = () => !!props.serverHealth(item)?.healthy
|
||||
@@ -130,7 +123,7 @@ export function HomeProjectsView(props: HomeProjectsViewProps) {
|
||||
server={item}
|
||||
{...props}
|
||||
{...contextMenuProps}
|
||||
selected={props.selection().server === ServerConnection.key(item) && !props.selection().directory}
|
||||
selected={props.selection.server === ServerConnection.key(item) && !props.selection.directory}
|
||||
collapsed={collapsed()}
|
||||
health={props.serverHealth(item)}
|
||||
/>
|
||||
@@ -277,8 +270,8 @@ function HomeServerRow(props: {
|
||||
<ServerRowMenuView
|
||||
server={props.server}
|
||||
labels={serverMenuLabels(props.language)}
|
||||
canDefault={props.canDefaultServer()}
|
||||
isDefault={props.defaultServerKey() === ServerConnection.key(props.server)}
|
||||
canDefault={props.canDefaultServer}
|
||||
isDefault={props.defaultServerKey === ServerConnection.key(props.server)}
|
||||
canRemove={props.canRemoveServer(props.server)}
|
||||
onEdit={props.onEditServer}
|
||||
onSetDefault={() => props.onSetDefaultServer(props.server)}
|
||||
@@ -339,7 +332,7 @@ function HomeProjectList(props: HomeProjectListProps) {
|
||||
const source = event.operation.source
|
||||
if (event.canceled || !isSortable(source)) return
|
||||
if (source.initialIndex !== source.index) props.onMoveProject(props.server, source.id.toString(), source.index)
|
||||
if (props.selection().server !== ServerConnection.key(props.server))
|
||||
if (props.selection.server !== ServerConnection.key(props.server))
|
||||
props.onSelectProject(props.server, source.id.toString())
|
||||
}}
|
||||
>
|
||||
@@ -350,7 +343,7 @@ function HomeProjectList(props: HomeProjectListProps) {
|
||||
row's sortable unregisters on unmount) and discarding animations.
|
||||
String keys keep row elements alive and move them on reorder. */}
|
||||
<For each={props.items.map((project) => project.worktree)}>
|
||||
{(worktree, index) => <HomeProjectSlot {...props} worktree={worktree} index={index} />}
|
||||
{(worktree, index) => <HomeProjectSlot {...props} worktree={worktree} index={index()} />}
|
||||
</For>
|
||||
</div>
|
||||
</DragDropProvider>
|
||||
@@ -360,7 +353,7 @@ function HomeProjectList(props: HomeProjectListProps) {
|
||||
function HomeProjectSlot(
|
||||
props: HomeProjectListProps & {
|
||||
worktree: string
|
||||
index: () => number
|
||||
index: number
|
||||
},
|
||||
) {
|
||||
const initial = props.items.find((item) => item.worktree === props.worktree)
|
||||
@@ -376,10 +369,9 @@ function HomeProjectSlot(
|
||||
project={project()}
|
||||
server={props.server}
|
||||
index={props.index}
|
||||
serverSelected={props.selection().server === ServerConnection.key(props.server)}
|
||||
serverSelected={props.selection.server === ServerConnection.key(props.server)}
|
||||
selected={
|
||||
props.selection().server === ServerConnection.key(props.server) &&
|
||||
props.selection().directory === props.worktree
|
||||
props.selection.server === ServerConnection.key(props.server) && props.selection.directory === props.worktree
|
||||
}
|
||||
unseen={props.unseenCount(props.server, project())}
|
||||
/>
|
||||
@@ -425,7 +417,7 @@ function HomeRecentlyClosedRow(
|
||||
) {
|
||||
const unreachable = () => props.serverHealth(props.server)?.healthy === false
|
||||
const path = () => {
|
||||
const home = props.homedir()
|
||||
const home = props.homedir
|
||||
const worktree = props.project.worktree
|
||||
if (home && (worktree === home || worktree.startsWith(`${home}/`))) return `~${worktree.slice(home.length)}`
|
||||
return worktree
|
||||
@@ -451,7 +443,7 @@ function HomeProjectRow(
|
||||
HomeProjectsContextMenuProps & {
|
||||
project: LocalProject
|
||||
server: ServerConnection.Any
|
||||
index: () => number
|
||||
index: number
|
||||
serverSelected: boolean
|
||||
selected: boolean
|
||||
unseen: number
|
||||
@@ -464,7 +456,7 @@ function HomeProjectRow(
|
||||
return props.project.worktree
|
||||
},
|
||||
get index() {
|
||||
return props.index()
|
||||
return props.index
|
||||
},
|
||||
})
|
||||
let pointerDownSelected: boolean | undefined
|
||||
|
||||
@@ -6,16 +6,16 @@ export function HomeProjects(props: { projects: HomeProjectsController; scroll:
|
||||
return (
|
||||
<HomeProjectsView
|
||||
language={props.projects.copy.language}
|
||||
servers={props.projects.server.list}
|
||||
projects={props.projects.project.list}
|
||||
recentlyClosed={props.projects.project.recentlyClosed}
|
||||
selection={props.projects.selection.value}
|
||||
homedir={props.projects.project.homedir}
|
||||
servers={props.projects.server.list()}
|
||||
projects={props.projects.project.list()}
|
||||
recentlyClosed={props.projects.project.recentlyClosed()}
|
||||
selection={props.projects.selection.value()}
|
||||
homedir={props.projects.project.homedir()}
|
||||
serverHealth={props.projects.server.health}
|
||||
projectsForServer={props.projects.server.projects}
|
||||
collapsed={props.projects.server.collapsed}
|
||||
canDefaultServer={props.projects.server.canDefault}
|
||||
defaultServerKey={props.projects.server.defaultKey}
|
||||
canDefaultServer={props.projects.server.canDefault()}
|
||||
defaultServerKey={props.projects.server.defaultKey()}
|
||||
canRevealProject={props.projects.project.canReveal}
|
||||
unseenCount={props.projects.project.unseenCount}
|
||||
onWheel={props.scroll.viewport.containWheel}
|
||||
|
||||
@@ -295,13 +295,13 @@ function groupSessions(records: HomeSessionRecord[], language: ReturnType<typeof
|
||||
export type HomeSessionsController = ReturnType<typeof createHomeSessionsController>
|
||||
|
||||
export function HomeSessionStatusController(props: {
|
||||
server: Accessor<ServerConnection.Key>
|
||||
server: ServerConnection.Key
|
||||
record: HomeSessionRecord
|
||||
isOpenTab: (record: HomeSessionRecord) => boolean
|
||||
render: (state: { unread: Accessor<boolean>; loading: Accessor<boolean>; open: Accessor<boolean> }) => JSX.Element
|
||||
}) {
|
||||
const avatar = useSessionTabAvatarState(
|
||||
props.server,
|
||||
() => props.server,
|
||||
() => props.record.session.location.directory,
|
||||
() => props.record.session.id,
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { SessionInfo } from "@opencode-ai/client/promise"
|
||||
import { type Accessor, createMemo, For, Show, Suspense } from "solid-js"
|
||||
import { createMemo, For, Show, Suspense } from "solid-js"
|
||||
import { Spinner } from "@opencode-ai/ui/spinner"
|
||||
import { ScrollView } from "@opencode-ai/ui/scroll-view"
|
||||
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
|
||||
@@ -38,17 +38,17 @@ function isBackgroundOpen(event: MouseEvent) {
|
||||
|
||||
export type HomeSessionsViewProps = {
|
||||
language: ReturnType<typeof useLanguage>
|
||||
groups: Accessor<HomeSessionGroup[]>
|
||||
showProjectName: Accessor<boolean>
|
||||
server: Accessor<ServerConnection.Key>
|
||||
canCreateSession: Accessor<boolean>
|
||||
searchValue: Accessor<string>
|
||||
searchPlaceholder: Accessor<string>
|
||||
searchOpen: Accessor<boolean>
|
||||
searchLoading: Accessor<boolean>
|
||||
searchResults: Accessor<HomeSessionRecord[]>
|
||||
searchActive: Accessor<string>
|
||||
searchNoResultsLabel: Accessor<string>
|
||||
groups: HomeSessionGroup[]
|
||||
showProjectName: boolean
|
||||
server: ServerConnection.Key
|
||||
canCreateSession: boolean
|
||||
searchValue: string
|
||||
searchPlaceholder: string
|
||||
searchOpen: boolean
|
||||
searchLoading: boolean
|
||||
searchResults: HomeSessionRecord[]
|
||||
searchActive: string
|
||||
searchNoResultsLabel: string
|
||||
titleOpacity: (id: HomeSessionGroup["id"]) => number
|
||||
isOpenTab: (record: HomeSessionRecord) => boolean
|
||||
onCreateSession: () => void
|
||||
@@ -81,7 +81,7 @@ export function HomeSessionsView(props: HomeSessionsViewProps) {
|
||||
<div class="sticky top-0 z-30 shrink-0 bg-v2-background-bg-base pb-3 pt-6 lg:pt-12" onWheel={props.onWheel}>
|
||||
<HomeSessionSearch {...props} />
|
||||
<Suspense>
|
||||
<Show when={props.groups().length > 0 && props.canCreateSession()}>
|
||||
<Show when={props.groups.length > 0 && props.canCreateSession}>
|
||||
<div class="pointer-events-none absolute right-0 top-[84px] z-20 flex lg:top-[108px]">
|
||||
<ButtonV2
|
||||
data-action="home-new-session"
|
||||
@@ -113,16 +113,16 @@ export function HomeSessionsView(props: HomeSessionsViewProps) {
|
||||
}
|
||||
>
|
||||
<Show
|
||||
when={props.groups().length > 0}
|
||||
when={props.groups.length > 0}
|
||||
fallback={
|
||||
<HomeSessionsEmpty
|
||||
onNewSession={props.canCreateSession() ? props.onCreateSession : undefined}
|
||||
onNewSession={props.canCreateSession ? props.onCreateSession : undefined}
|
||||
language={props.language}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div ref={props.onSetContent} class="flex flex-col pt-3 pr-3 pb-16">
|
||||
<For each={props.groups()}>
|
||||
<For each={props.groups}>
|
||||
{(group, index) => (
|
||||
<>
|
||||
<HomeSessionGroupHeader
|
||||
@@ -132,7 +132,7 @@ export function HomeSessionsView(props: HomeSessionsViewProps) {
|
||||
elevated={index() === 0}
|
||||
/>
|
||||
<div
|
||||
class={`flex min-w-0 flex-col gap-px pt-4 ${index() === props.groups().length - 1 ? "" : "mb-6"}`}
|
||||
class={`flex min-w-0 flex-col gap-px pt-4 ${index() === props.groups.length - 1 ? "" : "mb-6"}`}
|
||||
>
|
||||
<For each={group.sessions}>{(record) => <HomeSessionRow {...props} record={record} />}</For>
|
||||
</div>
|
||||
@@ -205,7 +205,7 @@ function HomeSessionSearch(props: HomeSessionsViewProps) {
|
||||
return (
|
||||
<div class="w-full">
|
||||
<div ref={props.onSetSearchRoot} data-component="home-session-search" class="relative z-30 w-full">
|
||||
<Show when={props.searchOpen()}>
|
||||
<Show when={props.searchOpen}>
|
||||
<div
|
||||
data-component="home-session-search-panel"
|
||||
class={`
|
||||
@@ -217,7 +217,7 @@ function HomeSessionSearch(props: HomeSessionsViewProps) {
|
||||
<div class="flex flex-col pt-9">
|
||||
<div id={HOME_SESSION_SEARCH_RESULTS_ID} role="listbox" class="flex flex-col gap-4 pt-4">
|
||||
<Show
|
||||
when={!props.searchLoading()}
|
||||
when={!props.searchLoading}
|
||||
fallback={
|
||||
<div class="flex items-center justify-center px-4 py-3 text-v2-text-text-muted [font-weight:440]">
|
||||
<Spinner class="size-4" />
|
||||
@@ -225,7 +225,7 @@ function HomeSessionSearch(props: HomeSessionsViewProps) {
|
||||
}
|
||||
>
|
||||
<Show
|
||||
when={props.searchResults().length > 0}
|
||||
when={props.searchResults.length > 0}
|
||||
fallback={
|
||||
<p
|
||||
class={`
|
||||
@@ -233,7 +233,7 @@ function HomeSessionSearch(props: HomeSessionsViewProps) {
|
||||
text-v2-text-text-muted [font-weight:440]
|
||||
`}
|
||||
>
|
||||
{props.searchNoResultsLabel()}
|
||||
{props.searchNoResultsLabel}
|
||||
</p>
|
||||
}
|
||||
>
|
||||
@@ -248,12 +248,12 @@ function HomeSessionSearch(props: HomeSessionsViewProps) {
|
||||
</p>
|
||||
<ScrollView class="max-h-80" viewportRef={props.onSetSearchList}>
|
||||
<div class="flex flex-col gap-px pb-2">
|
||||
<For each={props.searchResults()}>
|
||||
<For each={props.searchResults}>
|
||||
{(record) => (
|
||||
<HomeSessionSearchResultRow
|
||||
{...props}
|
||||
record={record}
|
||||
selected={props.searchActive() === homeSessionSearchKey(record)}
|
||||
selected={props.searchActive === homeSessionSearchKey(record)}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
@@ -280,16 +280,14 @@ function HomeSessionSearch(props: HomeSessionsViewProps) {
|
||||
relative z-20 min-w-0 flex-1 border-0 bg-transparent outline-0
|
||||
text-v2-text-text-base [font-weight:440] placeholder:text-v2-text-text-faint
|
||||
`}
|
||||
value={props.searchValue()}
|
||||
placeholder={props.searchPlaceholder()}
|
||||
aria-label={props.searchPlaceholder()}
|
||||
aria-expanded={props.searchOpen()}
|
||||
value={props.searchValue}
|
||||
placeholder={props.searchPlaceholder}
|
||||
aria-label={props.searchPlaceholder}
|
||||
aria-expanded={props.searchOpen}
|
||||
aria-controls={HOME_SESSION_SEARCH_RESULTS_ID}
|
||||
aria-autocomplete="list"
|
||||
aria-activedescendant={
|
||||
props.searchActive() && props.searchOpen()
|
||||
? `home-session-search-option-${props.searchActive()}`
|
||||
: undefined
|
||||
props.searchActive && props.searchOpen ? `home-session-search-option-${props.searchActive}` : undefined
|
||||
}
|
||||
onFocus={props.onSearchFocus}
|
||||
onInput={(event) => props.onSearchInput(event.currentTarget.value)}
|
||||
@@ -300,7 +298,7 @@ function HomeSessionSearch(props: HomeSessionsViewProps) {
|
||||
event.currentTarget.blur()
|
||||
return
|
||||
}
|
||||
if (!props.searchOpen() || props.searchResults().length === 0) return
|
||||
if (!props.searchOpen || props.searchResults.length === 0) return
|
||||
if (event.altKey || event.metaKey) return
|
||||
if (event.key === "ArrowDown") {
|
||||
event.preventDefault()
|
||||
@@ -318,14 +316,14 @@ function HomeSessionSearch(props: HomeSessionsViewProps) {
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Show when={props.searchValue()}>
|
||||
<Show when={props.searchValue}>
|
||||
<IconButtonV2
|
||||
type="button"
|
||||
variant="ghost-muted"
|
||||
size="small"
|
||||
class="relative z-20 shrink-0"
|
||||
icon={<IconV2 name="close" size="large" class="text-v2-icon-icon-muted" />}
|
||||
aria-label={props.searchPlaceholder()}
|
||||
aria-label={props.searchPlaceholder}
|
||||
onClick={() => {
|
||||
props.onSearchClose()
|
||||
props.onSearchFocus()
|
||||
@@ -345,7 +343,7 @@ function HomeSessionSearchResultRow(
|
||||
},
|
||||
) {
|
||||
const title = createMemo(() => sessionLabel(props.record.session))
|
||||
const showProjectName = () => props.showProjectName() && props.record.projectName
|
||||
const showProjectName = () => props.showProjectName && props.record.projectName
|
||||
const key = () => homeSessionSearchKey(props.record)
|
||||
|
||||
return (
|
||||
@@ -416,7 +414,7 @@ function HomeSessionGroupHeader(props: {
|
||||
|
||||
function HomeSessionRow(props: HomeSessionsViewProps & { record: HomeSessionRecord }) {
|
||||
const title = createMemo(() => sessionLabel(props.record.session))
|
||||
const showProjectName = () => props.showProjectName() && props.record.projectName
|
||||
const showProjectName = () => props.showProjectName && props.record.projectName
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -11,17 +11,17 @@ export function HomeSessions(props: {
|
||||
return (
|
||||
<HomeSessionsView
|
||||
language={props.sessions.copy.language}
|
||||
groups={props.sessions.data.groups}
|
||||
showProjectName={props.sessions.session.showProjectName}
|
||||
server={props.sessions.session.server}
|
||||
canCreateSession={props.sessions.session.canCreate}
|
||||
searchValue={props.search.query.value}
|
||||
searchPlaceholder={props.search.query.placeholder}
|
||||
searchOpen={props.search.query.open}
|
||||
searchLoading={props.search.result.loading}
|
||||
searchResults={props.search.result.list}
|
||||
searchActive={props.search.result.active}
|
||||
searchNoResultsLabel={props.search.result.noResultsLabel}
|
||||
groups={props.sessions.data.groups()}
|
||||
showProjectName={props.sessions.session.showProjectName()}
|
||||
server={props.sessions.session.server()}
|
||||
canCreateSession={props.sessions.session.canCreate()}
|
||||
searchValue={props.search.query.value()}
|
||||
searchPlaceholder={props.search.query.placeholder()}
|
||||
searchOpen={props.search.query.open()}
|
||||
searchLoading={props.search.result.loading()}
|
||||
searchResults={props.search.result.list()}
|
||||
searchActive={props.search.result.active()}
|
||||
searchNoResultsLabel={props.search.result.noResultsLabel()}
|
||||
titleOpacity={props.scroll.header.titleOpacity}
|
||||
isOpenTab={props.sessions.tab.isOpen}
|
||||
onCreateSession={props.sessions.session.create}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createEffect, Suspense, type ParentProps } from "solid-js"
|
||||
import { onMount, Show, Suspense, type ParentProps } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { DebugBar } from "@/components/debug-bar"
|
||||
import { Titlebar, type TitlebarUpdate } from "@/components/titlebar"
|
||||
@@ -9,15 +9,17 @@ export default function Layout(props: ParentProps) {
|
||||
const platform = usePlatform()
|
||||
const [state, setState] = createStore({ debugTools: true })
|
||||
|
||||
createEffect(() => setV2Toast(true))
|
||||
onMount(() => setV2Toast(true))
|
||||
|
||||
const update: TitlebarUpdate = {
|
||||
version: () => {
|
||||
get version() {
|
||||
const state = platform.updater?.state()
|
||||
if (state?.status !== "ready") return
|
||||
if (state?.status !== "ready") return undefined
|
||||
return state.version
|
||||
},
|
||||
installing: () => platform.updater?.state().status === "installing",
|
||||
get installing() {
|
||||
return platform.updater?.state().status === "installing"
|
||||
},
|
||||
install: () => void platform.updater?.install(),
|
||||
}
|
||||
|
||||
@@ -40,7 +42,9 @@ export default function Layout(props: ParentProps) {
|
||||
<main class="flex-1 min-h-0 min-w-0 overflow-x-hidden flex flex-col items-start contain-strict">
|
||||
<Suspense>{props.children}</Suspense>
|
||||
</main>
|
||||
{import.meta.env.DEV && state.debugTools && <DebugBar inline />}
|
||||
<Show when={import.meta.env.DEV && state.debugTools}>
|
||||
<DebugBar inline />
|
||||
</Show>
|
||||
<ToastRegion v2 />
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -40,7 +40,7 @@ export default function NewSessionPage() {
|
||||
return (
|
||||
<div class="relative size-full overflow-hidden flex flex-col">
|
||||
{suspendUntilPromptReady()}
|
||||
<NewSessionStatus mount={rightMount} visible={settings.visibility.status} />
|
||||
<NewSessionStatus mount={rightMount()} visible={settings.visibility.status()} />
|
||||
<div class="flex-1 min-h-0 flex flex-col gap-2 p-2">
|
||||
<NewSessionView input={draft.input} project={project} workspace={workspace} />
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
|
||||
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
|
||||
import { WordmarkV2 } from "@opencode-ai/ui/v2/wordmark-v2"
|
||||
import { Show, createMemo, createSignal, type Accessor } from "solid-js"
|
||||
import { Show, createMemo, createSignal } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { Portal } from "solid-js/web"
|
||||
import createPresence from "solid-presence"
|
||||
@@ -74,14 +74,14 @@ export function NewSessionView(props: {
|
||||
)
|
||||
}
|
||||
|
||||
export function NewSessionStatus(props: { mount: Accessor<HTMLElement | null>; visible: Accessor<boolean> }) {
|
||||
export function NewSessionStatus(props: { mount: HTMLElement | null; visible: boolean }) {
|
||||
const language = useLanguage()
|
||||
|
||||
return (
|
||||
<Show when={props.mount()} keyed>
|
||||
<Show when={props.mount} keyed>
|
||||
{(mount) => (
|
||||
<Portal mount={mount}>
|
||||
<Show when={props.visible()}>
|
||||
<Show when={props.visible}>
|
||||
<Tooltip placement="bottom" value={language.t("status.popover.trigger")}>
|
||||
<StatusPopoverV2 />
|
||||
</Tooltip>
|
||||
@@ -116,7 +116,7 @@ function ProviderTip() {
|
||||
})
|
||||
const openProviders = () => {
|
||||
void import("@/components/dialog-connect-provider").then(({ DialogConnectProvider }) => {
|
||||
void dialog.show(() => <DialogConnectProvider directory={() => sdk().directory} />)
|
||||
void dialog.show(() => <DialogConnectProvider directory={sdk().directory} />)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -163,7 +163,7 @@ export function TargetSessionRouteContent() {
|
||||
return (
|
||||
// Settings must keep the target-server SDK, sync, and models context and remain registered
|
||||
// when session content falls back to the route error boundary.
|
||||
<TargetServerScopedProviders directory={directory} sessionID={() => params.id}>
|
||||
<TargetServerScopedProviders directory={directory()} sessionID={params.id}>
|
||||
<TargetSessionSettingsCommand />
|
||||
<SessionRouteErrorBoundary sessionID={params.id} serverKey={requireServerKey(params.serverKey)} padded>
|
||||
<ResolvedTargetSessionRoute />
|
||||
@@ -183,17 +183,15 @@ export function SessionRouteErrorBoundary(
|
||||
const settings = useSettings()
|
||||
return (
|
||||
<ErrorBoundary
|
||||
fallback={(error) =>
|
||||
settings.general.newLayoutDesigns() ? (
|
||||
fallback={(error) => (
|
||||
<Show when={settings.general.newLayoutDesigns()} fallback={<ErrorPage error={error} />}>
|
||||
<SessionRouteFrame padded={props.padded}>
|
||||
<SessionPanelFrame newLayout raised={!!props.sessionID}>
|
||||
<SessionErrorFallback error={error} sessionID={props.sessionID} serverKey={props.serverKey} />
|
||||
</SessionPanelFrame>
|
||||
</SessionRouteFrame>
|
||||
) : (
|
||||
<ErrorPage error={error} />
|
||||
)
|
||||
}
|
||||
</Show>
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
</ErrorBoundary>
|
||||
@@ -253,7 +251,6 @@ function ResolvedTargetSessionRoute() {
|
||||
() => sync().session.lineage,
|
||||
)
|
||||
const directory = createMemo(() => current()?.session.location.directory)
|
||||
const targetDirectory = () => directory()!
|
||||
|
||||
createEffect(() => {
|
||||
const session = current()
|
||||
@@ -270,11 +267,13 @@ function ResolvedTargetSessionRoute() {
|
||||
// the terminal. Same-workspace tab switches keep it open because warm
|
||||
// targets resolve synchronously from the sync cache.
|
||||
<Show when={directory()}>
|
||||
<SDKProvider directory={targetDirectory}>
|
||||
<DirectoryDataProvider directory={targetDirectory} server={serverKey}>
|
||||
<TargetSessionPage />
|
||||
</DirectoryDataProvider>
|
||||
</SDKProvider>
|
||||
{(dir) => (
|
||||
<SDKProvider directory={dir()}>
|
||||
<DirectoryDataProvider directory={dir()} server={serverKey()}>
|
||||
<TargetSessionPage />
|
||||
</DirectoryDataProvider>
|
||||
</SDKProvider>
|
||||
)}
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
@@ -292,9 +291,7 @@ function TargetSessionPage() {
|
||||
)
|
||||
}
|
||||
|
||||
function TargetServerScopedProviders(
|
||||
props: ParentProps<{ directory?: () => string | undefined; sessionID?: () => string | undefined }>,
|
||||
) {
|
||||
function TargetServerScopedProviders(props: ParentProps<{ directory?: string; sessionID?: string }>) {
|
||||
return (
|
||||
<>
|
||||
<MarkSessionNotificationsViewed sessionID={props.sessionID} />
|
||||
@@ -303,10 +300,10 @@ function TargetServerScopedProviders(
|
||||
)
|
||||
}
|
||||
|
||||
function MarkSessionNotificationsViewed(props: { sessionID?: () => string | undefined }) {
|
||||
function MarkSessionNotificationsViewed(props: { sessionID?: string }) {
|
||||
const notification = useNotification()
|
||||
createEffect(() => {
|
||||
const sessionID = props.sessionID?.()
|
||||
const sessionID = props.sessionID
|
||||
if (!notification.ready() || !sessionID) return
|
||||
if (notification.session.unseenCount(sessionID) === 0) return
|
||||
notification.session.markViewed(sessionID)
|
||||
@@ -1238,8 +1235,8 @@ export default function Page() {
|
||||
<SessionReviewTab
|
||||
title={changesTitle()}
|
||||
empty={reviewEmpty(input)}
|
||||
diffs={reviewDiffs}
|
||||
view={controller.layout.view}
|
||||
diffs={reviewDiffs()}
|
||||
view={controller.layout.view()}
|
||||
diffStyle={input.diffStyle}
|
||||
onDiffStyleChange={input.onDiffStyleChange}
|
||||
onScrollRef={(el) => setTree("reviewScroll", el)}
|
||||
@@ -1272,8 +1269,12 @@ export default function Page() {
|
||||
get empty() {
|
||||
return reviewEmptyV2()
|
||||
},
|
||||
diffs: reviewDiffs,
|
||||
diffsReady: reviewReady,
|
||||
get diffs() {
|
||||
return reviewDiffs()
|
||||
},
|
||||
get diffsReady() {
|
||||
return reviewReady()
|
||||
},
|
||||
get diffVersion() {
|
||||
return vcsQuery.dataUpdatedAt
|
||||
},
|
||||
@@ -2072,11 +2073,11 @@ export default function Page() {
|
||||
onScheduleScrollState={scheduleScrollState}
|
||||
onAutoScrollHandleScroll={autoScroll.handleScroll}
|
||||
onMarkScrollGesture={markScrollGesture}
|
||||
hasScrollGesture={hasScrollGesture}
|
||||
hasScrollGesture={hasScrollGesture()}
|
||||
onUserScroll={markUserScroll}
|
||||
onHistoryScroll={onHistoryScroll}
|
||||
onAutoScrollInteraction={autoScroll.handleInteraction}
|
||||
shouldAnchorBottom={() =>
|
||||
shouldAnchorBottom={
|
||||
!location.hash && !store.messageId && !ui.pendingMessage && !autoScroll.userScrolled()
|
||||
}
|
||||
centered={centered()}
|
||||
@@ -2251,7 +2252,14 @@ export default function Page() {
|
||||
width: sessionPanelWidth(),
|
||||
}}
|
||||
>
|
||||
{settings.general.newLayoutDesigns() ? (
|
||||
<Show
|
||||
when={settings.general.newLayoutDesigns()}
|
||||
fallback={
|
||||
<SessionPanelFrame newLayout={false} raised={!!controller.identity.params.id}>
|
||||
{sessionPanelContent()}
|
||||
</SessionPanelFrame>
|
||||
}
|
||||
>
|
||||
<Show when={sessionPanelKey()} keyed>
|
||||
{(_) => (
|
||||
<SessionPanelFrame newLayout raised={!!controller.identity.params.id}>
|
||||
@@ -2259,11 +2267,7 @@ export default function Page() {
|
||||
</SessionPanelFrame>
|
||||
)}
|
||||
</Show>
|
||||
) : (
|
||||
<SessionPanelFrame newLayout={false} raised={!!controller.identity.params.id}>
|
||||
{sessionPanelContent()}
|
||||
</SessionPanelFrame>
|
||||
)}
|
||||
</Show>
|
||||
|
||||
<Show when={desktopSessionResizeOpen()}>
|
||||
<div onPointerDown={() => size.start()}>
|
||||
@@ -2287,13 +2291,13 @@ export default function Page() {
|
||||
<Show when={!newSessionDesign() && desktopSidePanelOpen()}>
|
||||
<Suspense>
|
||||
<SessionSidePanel
|
||||
canReview={canReview}
|
||||
diffs={reviewDiffs}
|
||||
diffsReady={reviewReady}
|
||||
empty={reviewEmptyText}
|
||||
hasReview={hasReview}
|
||||
reviewHasFocusableContent={hasReview}
|
||||
reviewCount={reviewCount}
|
||||
canReview={canReview()}
|
||||
diffs={reviewDiffs()}
|
||||
diffsReady={reviewReady()}
|
||||
empty={reviewEmptyText()}
|
||||
hasReview={hasReview()}
|
||||
reviewHasFocusableContent={hasReview()}
|
||||
reviewCount={reviewCount()}
|
||||
reviewPanel={reviewPanel}
|
||||
activeDiff={activeReviewFile()}
|
||||
focusReviewDiff={focusReviewDiff}
|
||||
@@ -2309,13 +2313,13 @@ export default function Page() {
|
||||
<div class="min-h-0 flex-1">
|
||||
<Suspense>
|
||||
<SessionSidePanel
|
||||
canReview={canReview}
|
||||
diffs={reviewDiffs}
|
||||
diffsReady={reviewReady}
|
||||
empty={reviewEmptyText}
|
||||
hasReview={hasReview}
|
||||
reviewHasFocusableContent={() => hasReview() || reviewV2State.sidebarOpened()}
|
||||
reviewCount={reviewCount}
|
||||
canReview={canReview()}
|
||||
diffs={reviewDiffs()}
|
||||
diffsReady={reviewReady()}
|
||||
empty={reviewEmptyText()}
|
||||
hasReview={hasReview()}
|
||||
reviewHasFocusableContent={hasReview() || reviewV2State.sidebarOpened()}
|
||||
reviewCount={reviewCount()}
|
||||
reviewPanel={reviewPanelV2}
|
||||
reviewSidebarToggle={(disabled) => (
|
||||
<SessionReviewV2SidebarToggle
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useSpring } from "@opencode-ai/ui/motion-spring"
|
||||
import { TextReveal } from "@opencode-ai/ui/text-reveal"
|
||||
import { TextStrikethrough } from "@opencode-ai/ui/text-strikethrough"
|
||||
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
||||
import { Index, createEffect, createMemo } from "solid-js"
|
||||
import { Index, Match, Switch, createEffect, createMemo } from "solid-js"
|
||||
import { Dynamic } from "solid-js/web"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { useLanguage } from "@/context/language"
|
||||
@@ -142,15 +142,16 @@ export function SessionTodoDock(props: {
|
||||
}}
|
||||
>
|
||||
<Index each={progress()}>
|
||||
{(item) =>
|
||||
item() === doneToken ? (
|
||||
<AnimatedNumber value={done()} />
|
||||
) : item() === totalToken ? (
|
||||
<AnimatedNumber value={total()} />
|
||||
) : (
|
||||
<span>{item()}</span>
|
||||
)
|
||||
}
|
||||
{(item) => (
|
||||
<Switch fallback={<span>{item()}</span>}>
|
||||
<Match when={item() === doneToken}>
|
||||
<AnimatedNumber value={done()} />
|
||||
</Match>
|
||||
<Match when={item() === totalToken}>
|
||||
<AnimatedNumber value={total()} />
|
||||
</Match>
|
||||
</Switch>
|
||||
)}
|
||||
</Index>
|
||||
</span>
|
||||
<div
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// @ts-nocheck
|
||||
import { createEffect, createMemo, onCleanup } from "solid-js"
|
||||
import { createEffect, createMemo, For, onCleanup } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import type { Todo } from "@/types"
|
||||
import { useServerSync } from "@/context/global-sync"
|
||||
@@ -326,11 +326,13 @@ export const Playground = {
|
||||
<button onClick={cycle} style={btn(step() > 0)}>
|
||||
Cycle progress ({step()}/3 done)
|
||||
</button>
|
||||
{[0, 1, 2, 3].map((value) => (
|
||||
<button onClick={() => setCfg("step", value)} style={btn(step() === value)}>
|
||||
{value} done
|
||||
</button>
|
||||
))}
|
||||
<For each={[0, 1, 2, 3]}>
|
||||
{(value) => (
|
||||
<button onClick={() => setCfg("step", value)} style={btn(step() === value)}>
|
||||
{value} done
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
|
||||
<div style={{ display: "grid", gap: "10px", "max-width": "560px" }}>
|
||||
|
||||
@@ -19,8 +19,8 @@ type ReviewDiff = FileDiffInfo
|
||||
export interface SessionReviewTabProps {
|
||||
title?: JSX.Element
|
||||
empty?: JSX.Element
|
||||
diffs: () => ReviewDiff[]
|
||||
view: () => ReturnType<ReturnType<typeof useLayout>["view"]>
|
||||
diffs: ReviewDiff[]
|
||||
view: ReturnType<ReturnType<typeof useLayout>["view"]>
|
||||
diffStyle: DiffStyle
|
||||
onDiffStyleChange?: (style: DiffStyle) => void
|
||||
onViewFile?: (file: string) => void
|
||||
@@ -77,7 +77,7 @@ export function SessionReviewTab(props: SessionReviewTabProps) {
|
||||
if (!el || !layout.ready() || userInteracted) return
|
||||
if (el.clientHeight === 0 || el.clientWidth === 0) return
|
||||
|
||||
const s = props.view().scroll("review")
|
||||
const s = props.view.scroll("review")
|
||||
if (!s || (s.x === 0 && s.y === 0)) return
|
||||
|
||||
const maxY = Math.max(0, el.scrollHeight - el.clientHeight)
|
||||
@@ -111,14 +111,14 @@ export function SessionReviewTab(props: SessionReviewTabProps) {
|
||||
if (!layout.ready()) return
|
||||
if (el.clientHeight === 0 || el.clientWidth === 0) return
|
||||
|
||||
props.view().setScroll("review", {
|
||||
props.view.setScroll("review", {
|
||||
x: el.scrollLeft,
|
||||
y: el.scrollTop,
|
||||
})
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
props.diffs().length
|
||||
props.diffs.length
|
||||
props.diffStyle
|
||||
if (!layout.ready()) return
|
||||
queueRestore()
|
||||
@@ -145,14 +145,14 @@ export function SessionReviewTab(props: SessionReviewTabProps) {
|
||||
}}
|
||||
onScroll={handleScroll}
|
||||
onDiffRendered={queueRestore}
|
||||
open={props.view().review.open()}
|
||||
onOpenChange={props.view().review.setOpen}
|
||||
open={props.view.review.open()}
|
||||
onOpenChange={props.view.review.setOpen}
|
||||
classes={{
|
||||
root: props.classes?.root ?? "pr-3",
|
||||
header: props.classes?.header ?? "px-3",
|
||||
container: props.classes?.container ?? "pl-3",
|
||||
}}
|
||||
diffs={props.diffs()}
|
||||
diffs={props.diffs}
|
||||
diffStyle={props.diffStyle}
|
||||
onDiffStyleChange={props.onDiffStyleChange}
|
||||
onViewFile={props.onViewFile}
|
||||
|
||||
@@ -65,13 +65,13 @@ function renderDiff(value: ReviewDiff): value is RenderDiff {
|
||||
}
|
||||
|
||||
export function SessionSidePanel(props: {
|
||||
canReview: () => boolean
|
||||
diffs: () => ReviewDiff[]
|
||||
diffsReady: () => boolean
|
||||
empty: () => string
|
||||
hasReview: () => boolean
|
||||
reviewHasFocusableContent: () => boolean
|
||||
reviewCount: () => number
|
||||
canReview: boolean
|
||||
diffs: ReviewDiff[]
|
||||
diffsReady: boolean
|
||||
empty: string
|
||||
hasReview: boolean
|
||||
reviewHasFocusableContent: boolean
|
||||
reviewCount: number
|
||||
reviewPanel: () => JSX.Element
|
||||
reviewSidebarToggle?: (disabled: boolean) => JSX.Element
|
||||
fileBrowserState?: SessionFileBrowserState
|
||||
@@ -113,7 +113,7 @@ export function SessionSidePanel(props: {
|
||||
})
|
||||
const treeWidth = createMemo(() => (fileOpen() ? `${fileTreeWidth()}px` : "0px"))
|
||||
|
||||
const diffs = createMemo(() => props.diffs().filter(renderDiff))
|
||||
const diffs = createMemo(() => props.diffs.filter(renderDiff))
|
||||
const diffFiles = createMemo(() => diffs().map((d) => d.file))
|
||||
const kinds = createMemo(() => {
|
||||
const merge = (a: "add" | "del" | "mix" | undefined, b: "add" | "del" | "mix") => {
|
||||
@@ -177,7 +177,7 @@ export function SessionSidePanel(props: {
|
||||
pathFromTab: file.pathFromTab,
|
||||
normalizeTab,
|
||||
review: reviewTab,
|
||||
hasReview: props.canReview,
|
||||
hasReview: () => props.canReview,
|
||||
fileBrowser: () => !!props.fileBrowserState,
|
||||
})
|
||||
const contextOpen = tabState.contextOpen
|
||||
@@ -348,7 +348,7 @@ export function SessionSidePanel(props: {
|
||||
onCleanup(stop)
|
||||
}}
|
||||
>
|
||||
<Show when={reviewTab() && props.canReview()}>
|
||||
<Show when={reviewTab() && props.canReview}>
|
||||
<Tabs.Trigger
|
||||
value="review"
|
||||
id={reviewTabID}
|
||||
@@ -356,8 +356,8 @@ export function SessionSidePanel(props: {
|
||||
>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<div>{language.t("session.tab.review")}</div>
|
||||
<Show when={props.hasReview()}>
|
||||
<div>{props.reviewCount()}</div>
|
||||
<Show when={props.hasReview}>
|
||||
<div>{props.reviewCount}</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Tabs.Trigger>
|
||||
@@ -463,12 +463,12 @@ export function SessionSidePanel(props: {
|
||||
</Tabs.List>
|
||||
</div>
|
||||
|
||||
<Show when={reviewTab() && props.canReview() && activeTab() === "review"}>
|
||||
<Show when={reviewTab() && props.canReview && activeTab() === "review"}>
|
||||
<div
|
||||
id={reviewTabPanelID}
|
||||
role="tabpanel"
|
||||
aria-labelledby={reviewTabID}
|
||||
tabIndex={props.reviewHasFocusableContent() ? undefined : 0}
|
||||
tabIndex={props.reviewHasFocusableContent ? undefined : 0}
|
||||
data-slot="tabs-content"
|
||||
class="flex flex-col h-full overflow-hidden contain-strict"
|
||||
>
|
||||
@@ -559,14 +559,14 @@ export function SessionSidePanel(props: {
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
<Show when={reviewTab() && props.canReview()}>
|
||||
<Show when={reviewTab() && props.canReview}>
|
||||
<Tabs.Trigger
|
||||
value="review"
|
||||
id={reviewTabID}
|
||||
aria-controls={activeTab() === "review" ? reviewTabPanelID : undefined}
|
||||
>
|
||||
{props.hasReview()
|
||||
? language.t("session.review.filesChanged", { count: props.reviewCount() })
|
||||
{props.hasReview
|
||||
? language.t("session.review.filesChanged", { count: props.reviewCount })
|
||||
: language.t("session.tab.review")}
|
||||
</Tabs.Trigger>
|
||||
</Show>
|
||||
@@ -611,7 +611,7 @@ export function SessionSidePanel(props: {
|
||||
fallback={
|
||||
<SortableTabV2
|
||||
tab={tab}
|
||||
index={() => tabs().all().indexOf(tab)}
|
||||
index={tabs().all().indexOf(tab)}
|
||||
temporary={temporaryTab() === tab}
|
||||
onTabClose={tabs().close}
|
||||
onTabDoubleClick={temporaryTab() === tab ? openTab : undefined}
|
||||
@@ -691,12 +691,12 @@ export function SessionSidePanel(props: {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show when={reviewTab() && props.canReview() && activeTab() === "review"}>
|
||||
<Show when={reviewTab() && props.canReview && activeTab() === "review"}>
|
||||
<div
|
||||
id={reviewTabPanelID}
|
||||
role="tabpanel"
|
||||
aria-labelledby={reviewTabID}
|
||||
tabIndex={props.reviewHasFocusableContent() ? undefined : 0}
|
||||
tabIndex={props.reviewHasFocusableContent ? undefined : 0}
|
||||
data-slot="tabs-content"
|
||||
class="flex flex-col h-full overflow-hidden contain-strict"
|
||||
>
|
||||
@@ -782,14 +782,14 @@ export function SessionSidePanel(props: {
|
||||
when={settings.general.newLayoutDesigns()}
|
||||
fallback={
|
||||
<>
|
||||
{props.reviewCount()}{" "}
|
||||
{props.reviewCount}{" "}
|
||||
{language.t(
|
||||
props.reviewCount() === 1 ? "session.review.change.one" : "session.review.change.other",
|
||||
props.reviewCount === 1 ? "session.review.change.one" : "session.review.change.other",
|
||||
)}
|
||||
</>
|
||||
}
|
||||
>
|
||||
{language.t("session.review.filesChanged", { count: props.reviewCount() })}
|
||||
{language.t("session.review.filesChanged", { count: props.reviewCount })}
|
||||
</Show>
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger value="all" class="flex-1" classes={{ button: "w-full" }}>
|
||||
@@ -799,9 +799,9 @@ export function SessionSidePanel(props: {
|
||||
<Show when={fileTreeTab() === "changes"}>
|
||||
<Tabs.Content value="changes" class="bg-background-stronger px-3 py-0">
|
||||
<Switch>
|
||||
<Match when={props.hasReview() || !props.diffsReady()}>
|
||||
<Match when={props.hasReview || !props.diffsReady}>
|
||||
<Show
|
||||
when={props.diffsReady()}
|
||||
when={props.diffsReady}
|
||||
fallback={
|
||||
<div class="px-2 py-2 text-12-regular text-text-weak">
|
||||
{language.t("common.loading")}
|
||||
|
||||
@@ -273,7 +273,7 @@ export function TerminalPanelV2(props: { stacked?: boolean } = {}) {
|
||||
>
|
||||
<For each={all()}>
|
||||
{(pty, index) => (
|
||||
<SortableTerminalTabV2 terminal={pty} index={index} newLayout={newLayout()} onClose={close} />
|
||||
<SortableTerminalTabV2 terminal={pty} index={index()} newLayout={newLayout()} onClose={close} />
|
||||
)}
|
||||
</For>
|
||||
<div class="h-full flex items-center justify-center">
|
||||
|
||||
@@ -210,11 +210,11 @@ type MessageTimelineProps = {
|
||||
onScheduleScrollState: (el: HTMLDivElement) => void
|
||||
onAutoScrollHandleScroll: () => void
|
||||
onMarkScrollGesture: (target?: EventTarget | null) => void
|
||||
hasScrollGesture: () => boolean
|
||||
hasScrollGesture: boolean
|
||||
onUserScroll: () => void
|
||||
onHistoryScroll: () => void
|
||||
onAutoScrollInteraction: (event: MouseEvent) => void
|
||||
shouldAnchorBottom: () => boolean
|
||||
shouldAnchorBottom: boolean
|
||||
centered: boolean
|
||||
setContentRef: (el: HTMLDivElement) => void
|
||||
userMessages: UserMessage[]
|
||||
@@ -243,7 +243,7 @@ function MessageTimelineView(
|
||||
const ownerSessionKey = props.data.sessionKey()
|
||||
const cached = timelineCache.get(ownerSessionKey)
|
||||
const initialMeasurements = cached?.measurements
|
||||
const coldBottomMount = !initialMeasurements?.length && props.shouldAnchorBottom()
|
||||
const coldBottomMount = !initialMeasurements?.length && props.shouldAnchorBottom
|
||||
|
||||
const [listRoot, setListRoot] = createSignal<HTMLDivElement>()
|
||||
const sessionID = props.data.sessionID
|
||||
@@ -338,7 +338,7 @@ function MessageTimelineView(
|
||||
},
|
||||
getScrollElement: () => listRoot() ?? null,
|
||||
observeElementOffset: observeElementOffsetReconnectAware,
|
||||
initialOffset: () => (props.shouldAnchorBottom() ? Number.MAX_SAFE_INTEGER : 0),
|
||||
initialOffset: () => (props.shouldAnchorBottom ? Number.MAX_SAFE_INTEGER : 0),
|
||||
initialMeasurementsCache: initialMeasurements,
|
||||
estimateSize: () => timelineFallbackItemSize,
|
||||
scrollToFn: (offset, options, instance) => {
|
||||
@@ -376,11 +376,11 @@ function MessageTimelineView(
|
||||
const resizeItem = virtualizer.resizeItem
|
||||
let resizeAnchorScheduled = false
|
||||
const anchorResizedBottom = () => {
|
||||
if (resizeAnchorScheduled || props.hasScrollGesture()) return
|
||||
if (resizeAnchorScheduled || props.hasScrollGesture) return
|
||||
resizeAnchorScheduled = true
|
||||
queueMicrotask(() => {
|
||||
resizeAnchorScheduled = false
|
||||
if (!props.shouldAnchorBottom() || props.hasScrollGesture()) return
|
||||
if (!props.shouldAnchorBottom || props.hasScrollGesture) return
|
||||
virtualizer.scrollToEnd()
|
||||
})
|
||||
}
|
||||
@@ -405,10 +405,10 @@ function MessageTimelineView(
|
||||
})
|
||||
}
|
||||
resizeItem(index, size)
|
||||
if (root && props.shouldAnchorBottom()) anchorResizedBottom()
|
||||
if (root && props.shouldAnchorBottom) anchorResizedBottom()
|
||||
}
|
||||
virtualizer.shouldAdjustScrollPositionOnItemSizeChange = (item) => {
|
||||
if (props.shouldAnchorBottom()) return false
|
||||
if (props.shouldAnchorBottom) return false
|
||||
const first = virtualizer.range?.startIndex
|
||||
return first !== undefined && item.index < first
|
||||
}
|
||||
@@ -429,18 +429,18 @@ function MessageTimelineView(
|
||||
let overscanFrame: number | undefined
|
||||
onMount(() => {
|
||||
overscanFrame = requestAnimationFrame(() => {
|
||||
if (props.shouldAnchorBottom()) virtualizer.scrollToEnd()
|
||||
if (props.shouldAnchorBottom) virtualizer.scrollToEnd()
|
||||
overscanFrame = requestAnimationFrame(() => {
|
||||
overscanFrame = undefined
|
||||
if (renderOverscan() < 20) setRenderOverscan(20)
|
||||
if (props.shouldAnchorBottom()) virtualizer.scrollToEnd()
|
||||
if (props.shouldAnchorBottom) virtualizer.scrollToEnd()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const maybeAnchorBottom = () => {
|
||||
if (timelineRows().length === 0) return
|
||||
if (!props.shouldAnchorBottom() || props.hasScrollGesture()) return
|
||||
if (!props.shouldAnchorBottom || props.hasScrollGesture) return
|
||||
if (resizePinFrame !== undefined) cancelAnimationFrame(resizePinFrame)
|
||||
clearPrependAnchor()
|
||||
if (prependAnchorFrame !== undefined) cancelAnimationFrame(prependAnchorFrame)
|
||||
@@ -552,7 +552,7 @@ function MessageTimelineView(
|
||||
if (prependLoading) updatePrependAnchor()
|
||||
props.onScheduleScrollState(event.currentTarget)
|
||||
props.onHistoryScroll()
|
||||
if (!props.hasScrollGesture()) return
|
||||
if (!props.hasScrollGesture) return
|
||||
props.onUserScroll()
|
||||
props.onAutoScrollHandleScroll()
|
||||
props.onMarkScrollGesture(event.currentTarget)
|
||||
@@ -709,21 +709,21 @@ function MessageTimelineView(
|
||||
)
|
||||
}
|
||||
|
||||
function TimelineRowFrame(input: { row: Accessor<FramedTimelineRow>; children: JSX.Element }) {
|
||||
function TimelineRowFrame(input: { row: FramedTimelineRow; children: JSX.Element }) {
|
||||
const anchor = () => {
|
||||
const row = input.row()
|
||||
const row = input.row
|
||||
return row._tag === "CommentStrip" || (row._tag === "UserMessage" && row.anchor)
|
||||
}
|
||||
const previousAssistantPart = () => {
|
||||
const row = input.row()
|
||||
const row = input.row
|
||||
return row._tag === "AssistantPart" && row.previousAssistantPart
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
id={anchor() ? props.anchor(input.row().userMessageID) : undefined}
|
||||
data-message-id={input.row().userMessageID}
|
||||
data-timeline-row={input.row()._tag}
|
||||
id={anchor() ? props.anchor(input.row.userMessageID) : undefined}
|
||||
data-message-id={input.row.userMessageID}
|
||||
data-timeline-row={input.row._tag}
|
||||
classList={{
|
||||
"min-w-0 w-full max-w-full": true,
|
||||
"md:max-w-200 2xl:max-w-[1000px]": props.centered,
|
||||
@@ -748,7 +748,7 @@ function MessageTimelineView(
|
||||
getMsgParts(commentStripRow().userMessageID).flatMap((part) => MessageComment.fromPart(part) ?? []),
|
||||
)
|
||||
return (
|
||||
<TimelineRowFrame row={commentStripRow}>
|
||||
<TimelineRowFrame row={commentStripRow()}>
|
||||
<div class="w-full px-4 md:px-5 pb-2">
|
||||
<div class="ms-auto max-w-[82%] overflow-x-auto no-scrollbar">
|
||||
<div class="flex w-max min-w-full justify-end gap-2">
|
||||
@@ -797,7 +797,7 @@ function MessageTimelineView(
|
||||
return getMsgParts(userMessageRow().userMessageID).flatMap((part) => MessageComment.fromPart(part) ?? [])
|
||||
})
|
||||
return (
|
||||
<TimelineRowFrame row={userMessageRow}>
|
||||
<TimelineRowFrame row={userMessageRow()}>
|
||||
<Show when={message()}>
|
||||
{(message) => (
|
||||
<div data-slot="session-turn-message-container" class="w-full px-4 md:px-5">
|
||||
@@ -819,7 +819,7 @@ function MessageTimelineView(
|
||||
case "TurnDivider": {
|
||||
const turnDividerRow = row as Accessor<TimelineRowByTag<"TurnDivider">>
|
||||
return (
|
||||
<TimelineRowFrame row={turnDividerRow}>
|
||||
<TimelineRowFrame row={turnDividerRow()}>
|
||||
<div data-slot="session-turn-message-container" class="w-full px-4 md:px-5">
|
||||
<div data-slot="session-turn-compaction">
|
||||
<MessageDivider
|
||||
@@ -835,7 +835,7 @@ function MessageTimelineView(
|
||||
case "AssistantPart": {
|
||||
const assistantPartRow = row as Accessor<TimelineRowByTag<"AssistantPart">>
|
||||
return (
|
||||
<TimelineRowFrame row={assistantPartRow}>
|
||||
<TimelineRowFrame row={assistantPartRow()}>
|
||||
<div data-slot="session-turn-message-container" class="w-full px-4 md:px-5">
|
||||
<div
|
||||
data-slot="session-turn-assistant-content"
|
||||
@@ -850,7 +850,7 @@ function MessageTimelineView(
|
||||
case "Thinking": {
|
||||
const thinkingRow = row as Accessor<TimelineRowByTag<"Thinking">>
|
||||
return (
|
||||
<TimelineRowFrame row={thinkingRow}>
|
||||
<TimelineRowFrame row={thinkingRow()}>
|
||||
<div data-slot="session-turn-message-container" class="w-full px-4 md:px-5">
|
||||
<TimelineThinkingRow
|
||||
reasoningHeading={thinkingRow().reasoningHeading}
|
||||
@@ -863,7 +863,7 @@ function MessageTimelineView(
|
||||
case "Retry": {
|
||||
const retryRow = row as Accessor<TimelineRowByTag<"Retry">>
|
||||
return (
|
||||
<TimelineRowFrame row={retryRow}>
|
||||
<TimelineRowFrame row={retryRow()}>
|
||||
<div data-slot="session-turn-message-container" class="w-full px-4 md:px-5">
|
||||
<SessionRetry status={sessionStatus()} show={activeMessageID() === retryRow().userMessageID} />
|
||||
</div>
|
||||
@@ -873,7 +873,7 @@ function MessageTimelineView(
|
||||
case "DiffSummary": {
|
||||
const diffSummaryRow = row as Accessor<TimelineRowByTag<"DiffSummary">>
|
||||
return (
|
||||
<TimelineRowFrame row={diffSummaryRow}>
|
||||
<TimelineRowFrame row={diffSummaryRow()}>
|
||||
<div data-slot="session-turn-message-container" class="w-full px-4 md:px-5">
|
||||
<TimelineDiffSummaryRow diffs={diffSummaryRow().diffs} />
|
||||
</div>
|
||||
@@ -883,7 +883,7 @@ function MessageTimelineView(
|
||||
case "Error": {
|
||||
const errorRow = row as Accessor<TimelineRowByTag<"Error">>
|
||||
return (
|
||||
<TimelineRowFrame row={errorRow}>
|
||||
<TimelineRowFrame row={errorRow()}>
|
||||
<div data-slot="session-turn-message-container" class="w-full px-4 md:px-5">
|
||||
<Card variant="error" class="error-card">
|
||||
{errorRow().text}
|
||||
|
||||
@@ -35,8 +35,8 @@ type ReviewDiff = FileDiffInfo
|
||||
export type ReviewPanelV2Props = {
|
||||
title?: JSX.Element
|
||||
empty?: JSX.Element
|
||||
diffs: () => ReviewDiff[]
|
||||
diffsReady: () => boolean
|
||||
diffs: ReviewDiff[]
|
||||
diffsReady: boolean
|
||||
diffVersion?: number
|
||||
loadDiff?: (path: string, version?: number) => Promise<RenderDiff | undefined>
|
||||
activeFile?: string
|
||||
@@ -56,7 +56,7 @@ export type ReviewPanelV2Props = {
|
||||
export function ReviewPanelV2(props: ReviewPanelV2Props) {
|
||||
const sdk = useSDK()
|
||||
|
||||
const diffs = createMemo(() => props.diffs().filter(filterRenderableDiff))
|
||||
const diffs = createMemo(() => props.diffs.filter(filterRenderableDiff))
|
||||
const filteredFiles = createMemo(() =>
|
||||
filterReviewFiles(
|
||||
diffs().map((diff) => diff.file),
|
||||
@@ -122,11 +122,11 @@ export function ReviewPanelV2(props: ReviewPanelV2Props) {
|
||||
state={props.state}
|
||||
diffsReady={props.diffsReady}
|
||||
onSelectFile={props.onSelectFile}
|
||||
diffs={diffs}
|
||||
filteredFiles={filteredFiles}
|
||||
searching={searching}
|
||||
kinds={treeKinds}
|
||||
activeDiff={activeDiff}
|
||||
diffs={diffs()}
|
||||
filteredFiles={filteredFiles()}
|
||||
searching={searching()}
|
||||
kinds={treeKinds()}
|
||||
activeDiff={activeDiff()}
|
||||
/>
|
||||
}
|
||||
activeFile={activeDiff()}
|
||||
@@ -170,19 +170,19 @@ export function ReviewPanelV2(props: ReviewPanelV2Props) {
|
||||
function ReviewPanelV2Sidebar(props: {
|
||||
title?: JSX.Element
|
||||
state: ReviewPanelV2State
|
||||
diffsReady: () => boolean
|
||||
diffsReady: boolean
|
||||
onSelectFile: (path: string) => void
|
||||
diffs: () => RenderDiff[]
|
||||
filteredFiles: () => string[]
|
||||
searching: () => boolean
|
||||
kinds: () => ReturnType<typeof reviewDiffKinds>
|
||||
activeDiff: () => string | undefined
|
||||
diffs: RenderDiff[]
|
||||
filteredFiles: string[]
|
||||
searching: boolean
|
||||
kinds: ReturnType<typeof reviewDiffKinds>
|
||||
activeDiff: string | undefined
|
||||
}) {
|
||||
const language = useLanguage()
|
||||
const [explicitHighlight, setExplicitHighlight] = createSignal<string | undefined>()
|
||||
const highlightedPath = createMemo(() => {
|
||||
if (!props.searching()) return undefined
|
||||
const files = props.filteredFiles()
|
||||
if (!props.searching) return undefined
|
||||
const files = props.filteredFiles
|
||||
if (files.length === 0) return undefined
|
||||
const explicit = explicitHighlight()
|
||||
if (explicit && files.includes(explicit)) return explicit
|
||||
@@ -190,8 +190,8 @@ function ReviewPanelV2Sidebar(props: {
|
||||
})
|
||||
|
||||
const onFilterKeyDown = (event: KeyboardEvent & { currentTarget: HTMLInputElement }) => {
|
||||
if (!props.searching()) return
|
||||
applyFileListKeyDown(event, props.filteredFiles(), highlightedPath(), {
|
||||
if (!props.searching) return
|
||||
applyFileListKeyDown(event, props.filteredFiles, highlightedPath(), {
|
||||
onHighlight: setExplicitHighlight,
|
||||
onSelect: props.onSelectFile,
|
||||
})
|
||||
@@ -202,7 +202,7 @@ function ReviewPanelV2Sidebar(props: {
|
||||
open={props.state.sidebarOpened()}
|
||||
transition={props.state.sidebarTransition()}
|
||||
title={props.title}
|
||||
stats={<DiffChanges changes={props.diffs()} />}
|
||||
stats={<DiffChanges changes={props.diffs} />}
|
||||
filter={props.state.filter()}
|
||||
onFilterChange={props.state.setFilter}
|
||||
onFilterKeyDown={onFilterKeyDown}
|
||||
@@ -212,7 +212,7 @@ function ReviewPanelV2Sidebar(props: {
|
||||
maxWidth={SESSION_REVIEW_V2_SIDEBAR_WIDTH_MAX}
|
||||
>
|
||||
<Show
|
||||
when={props.diffsReady()}
|
||||
when={props.diffsReady}
|
||||
fallback={
|
||||
<div class="px-2 py-2 text-12-regular text-text-weak">
|
||||
{language.t("common.loading")}
|
||||
@@ -221,25 +221,25 @@ function ReviewPanelV2Sidebar(props: {
|
||||
}
|
||||
>
|
||||
<Show
|
||||
when={props.searching()}
|
||||
when={props.searching}
|
||||
fallback={
|
||||
<FileTreeV2
|
||||
allowed={props.filteredFiles()}
|
||||
kinds={props.kinds()}
|
||||
allowed={props.filteredFiles}
|
||||
kinds={props.kinds}
|
||||
draggable={false}
|
||||
active={props.activeDiff()}
|
||||
active={props.activeDiff}
|
||||
onFileClick={(node) => props.onSelectFile(node.path)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Show
|
||||
when={props.filteredFiles().length > 0}
|
||||
when={props.filteredFiles.length > 0}
|
||||
fallback={<div class="px-2 py-2 text-12-regular text-text-weak">{language.t("palette.empty")}</div>}
|
||||
>
|
||||
<SessionFileListV2
|
||||
files={props.filteredFiles()}
|
||||
kinds={props.kinds()}
|
||||
active={props.activeDiff()}
|
||||
files={props.filteredFiles}
|
||||
kinds={props.kinds}
|
||||
active={props.activeDiff}
|
||||
highlighted={highlightedPath()}
|
||||
onFileClick={(path) => {
|
||||
setExplicitHighlight(path)
|
||||
|
||||
@@ -22,6 +22,7 @@ await rm(outdir, { recursive: true, force: true })
|
||||
|
||||
const singleFlag = process.argv.includes("--single")
|
||||
const baselineFlag = process.argv.includes("--baseline")
|
||||
const requestedTarget = process.argv.find((arg) => arg.startsWith("--target="))?.slice("--target=".length)
|
||||
const skipInstall = process.argv.includes("--skip-install")
|
||||
const skipWebUi = process.argv.includes("--skip-web-ui")
|
||||
const solidPlugin = createSolidTransformPlugin()
|
||||
@@ -46,13 +47,17 @@ const allTargets: {
|
||||
{ os: "win32", arch: "x64", avx2: false },
|
||||
]
|
||||
|
||||
const targets = singleFlag
|
||||
? allTargets.filter((item) => {
|
||||
if (item.os !== process.platform || item.arch !== process.arch) return false
|
||||
if (item.avx2 === false) return baselineFlag
|
||||
return item.abi === undefined
|
||||
})
|
||||
: allTargets
|
||||
const targets =
|
||||
requestedTarget !== undefined
|
||||
? allTargets.filter((item) => targetName(item) === requestedTarget)
|
||||
: singleFlag
|
||||
? allTargets.filter((item) => {
|
||||
if (item.os !== process.platform || item.arch !== process.arch) return false
|
||||
if (item.avx2 === false) return baselineFlag
|
||||
return item.abi === undefined
|
||||
})
|
||||
: allTargets
|
||||
if (!targets.length) throw new Error(`Unknown build target: ${requestedTarget}`)
|
||||
|
||||
if (!skipInstall) await $`bun install --os="*" --cpu="*" @opentui/core@${pkg.dependencies["@opentui/core"]}`
|
||||
const appArchive = await buildAppArchive(Script.channel, { skipBuild: skipWebUi })
|
||||
@@ -81,15 +86,7 @@ for (const item of targets) {
|
||||
}))
|
||||
},
|
||||
}
|
||||
const target = [
|
||||
binary,
|
||||
item.os === "win32" ? "windows" : item.os,
|
||||
item.arch,
|
||||
item.avx2 === false ? "baseline" : undefined,
|
||||
item.abi,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("-")
|
||||
const target = targetName(item)
|
||||
const name = target.replace(binary, "cli")
|
||||
console.log(`building ${name}`)
|
||||
const result = await Bun.build({
|
||||
@@ -143,3 +140,15 @@ for (const item of targets) {
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
function targetName(item: (typeof allTargets)[number]) {
|
||||
return [
|
||||
binary,
|
||||
item.os === "win32" ? "windows" : item.os,
|
||||
item.arch,
|
||||
item.avx2 === false ? "baseline" : undefined,
|
||||
item.abi,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("-")
|
||||
}
|
||||
|
||||
@@ -538,9 +538,8 @@ async function replayMessage(
|
||||
}
|
||||
|
||||
function matchesStart(event: EventSubscribeOutput, start: TurnStart) {
|
||||
if (start.type === "input") return event.type === "session.input.promoted" && event.data.inputID === start.id
|
||||
if (start.type === "compaction")
|
||||
return event.type === "session.compaction.admitted" && event.data.inputID === start.id
|
||||
if (start.type === "input") return event.type === "session.inbox.delivered" && event.data.inboxID === start.id
|
||||
if (start.type === "compaction") return event.type === "session.inbox.delivered" && event.data.inboxID === start.id
|
||||
return event.type === "session.skill.activated" && event.id === start.id.replace(/^msg_/, "evt_")
|
||||
}
|
||||
|
||||
|
||||
@@ -195,8 +195,8 @@ export async function runNonInteractivePrompt(input: Input) {
|
||||
if (!("sessionID" in event.data) || event.data.sessionID !== input.sessionID) continue
|
||||
const time = toMillis("created" in event ? event.created : undefined)
|
||||
|
||||
if (event.type === "session.input.promoted") {
|
||||
if (event.data.inputID === messageID) {
|
||||
if (event.type === "session.inbox.delivered") {
|
||||
if (event.data.inboxID === messageID) {
|
||||
promoted = true
|
||||
prePromotionError = undefined
|
||||
continue
|
||||
|
||||
@@ -447,7 +447,7 @@ function UpdateFooter(props: {
|
||||
})
|
||||
|
||||
return (
|
||||
<box width="100%" height={4} flexDirection="row" gap={1} live={props.animating()}>
|
||||
<box width="100%" height={4} flexDirection="row" gap={1} paddingLeft={1} live={props.animating()}>
|
||||
<Monogram ink={monogramInk} />
|
||||
<box flexDirection="column" flexGrow={1} overflow="hidden">
|
||||
<CellLine cells={header()} />
|
||||
|
||||
@@ -22,8 +22,8 @@ describe("acp event behavior", () => {
|
||||
delta: "before admission",
|
||||
}),
|
||||
)
|
||||
send(durableEvent("session.input.promoted", { sessionID: "ses_b", inputID: id }))
|
||||
send(durableEvent("session.input.promoted", { sessionID: "ses_a", inputID: "input_other" }))
|
||||
send(durableEvent("session.inbox.delivered", { sessionID: "ses_b", inboxID: id }))
|
||||
send(durableEvent("session.inbox.delivered", { sessionID: "ses_a", inboxID: "input_other" }))
|
||||
send(
|
||||
ephemeralEvent("session.text.delta", {
|
||||
sessionID: "ses_a",
|
||||
@@ -32,7 +32,7 @@ describe("acp event behavior", () => {
|
||||
delta: "wrong input",
|
||||
}),
|
||||
)
|
||||
send(durableEvent("session.input.promoted", { sessionID: "ses_a", inputID: id }))
|
||||
send(durableEvent("session.inbox.delivered", { sessionID: "ses_a", inboxID: id }))
|
||||
send(
|
||||
ephemeralEvent("session.text.delta", {
|
||||
sessionID: "ses_b",
|
||||
@@ -68,7 +68,7 @@ describe("acp event behavior", () => {
|
||||
fixture,
|
||||
connection: recordingConnection(updates),
|
||||
sessionID: "ses_a",
|
||||
inputID: "input_a",
|
||||
inboxID: "input_a",
|
||||
})
|
||||
|
||||
expect(fixture.requests.slice(0, 2).map((request) => request.path)).toEqual([
|
||||
@@ -99,7 +99,7 @@ describe("acp event behavior", () => {
|
||||
const updates: SessionUpdateParams[] = []
|
||||
const fixture = createSseFixture({
|
||||
async onPrompt({ id, send }) {
|
||||
send(durableEvent("session.input.promoted", { sessionID: "ses_order", inputID: id }))
|
||||
send(durableEvent("session.inbox.delivered", { sessionID: "ses_order", inboxID: id }))
|
||||
send(
|
||||
ephemeralEvent("session.reasoning.delta", {
|
||||
sessionID: "ses_order",
|
||||
@@ -148,7 +148,7 @@ describe("acp event behavior", () => {
|
||||
},
|
||||
requestPermission: async () => ({ outcome: { outcome: "cancelled" } }),
|
||||
} satisfies Connection
|
||||
const result = turn({ fixture, connection, sessionID: "ses_order", inputID: "input_order" })
|
||||
const result = turn({ fixture, connection, sessionID: "ses_order", inboxID: "input_order" })
|
||||
|
||||
try {
|
||||
await withTimeout(firstUpdate.promise, "first ordered update was not delivered")
|
||||
@@ -195,7 +195,7 @@ describe("acp event behavior", () => {
|
||||
const updates: SessionUpdateParams[] = []
|
||||
const fixture = createSseFixture({
|
||||
onPrompt({ id, send }) {
|
||||
send(durableEvent("session.input.promoted", { sessionID: "ses_parent", inputID: id }))
|
||||
send(durableEvent("session.inbox.delivered", { sessionID: "ses_parent", inboxID: id }))
|
||||
send(
|
||||
durableEvent("session.created", {
|
||||
sessionID: "ses_child",
|
||||
@@ -240,7 +240,7 @@ describe("acp event behavior", () => {
|
||||
fixture,
|
||||
connection: recordingConnection(updates),
|
||||
sessionID: "ses_parent",
|
||||
inputID: "input_parent",
|
||||
inboxID: "input_parent",
|
||||
})
|
||||
|
||||
expect(updates.map((item) => [item.sessionId, item.update.sessionUpdate])).toEqual([
|
||||
@@ -276,7 +276,7 @@ describe("acp event behavior", () => {
|
||||
const completed = Promise.withResolvers<void>()
|
||||
const fixture = createSseFixture({
|
||||
onPrompt({ id, send }) {
|
||||
send(durableEvent("session.input.promoted", { sessionID: "ses_parent", inputID: id }))
|
||||
send(durableEvent("session.inbox.delivered", { sessionID: "ses_parent", inboxID: id }))
|
||||
send(
|
||||
durableEvent("session.created", {
|
||||
sessionID: "ses_background",
|
||||
@@ -292,7 +292,7 @@ describe("acp event behavior", () => {
|
||||
fixture,
|
||||
connection: recordingConnection(updates),
|
||||
sessionID: "ses_parent",
|
||||
inputID: "input_parent",
|
||||
inboxID: "input_parent",
|
||||
childSessionUpdate: async (update) => {
|
||||
childUpdates.push(update)
|
||||
if (update.type === "status" && update.status === "completed") completed.resolve()
|
||||
@@ -370,7 +370,7 @@ describe("acp event behavior", () => {
|
||||
const updates: SessionUpdateParams[] = []
|
||||
const fixture = createSseFixture({
|
||||
onPrompt({ id, send }) {
|
||||
send(durableEvent("session.input.promoted", { sessionID: "ses_tools", inputID: id }))
|
||||
send(durableEvent("session.inbox.delivered", { sessionID: "ses_tools", inboxID: id }))
|
||||
send(
|
||||
durableEvent("session.tool.input.started", {
|
||||
sessionID: "ses_tools",
|
||||
@@ -460,7 +460,7 @@ describe("acp event behavior", () => {
|
||||
fixture,
|
||||
connection: recordingConnection(updates),
|
||||
sessionID: "ses_tools",
|
||||
inputID: "input_tools",
|
||||
inboxID: "input_tools",
|
||||
})
|
||||
|
||||
expect(
|
||||
@@ -597,7 +597,7 @@ describe("acp event behavior", () => {
|
||||
const control: TurnControl = { cancelled: false, admission: new AbortController() }
|
||||
const fixture = createSseFixture({
|
||||
onPrompt({ id, send }) {
|
||||
send(durableEvent("session.input.promoted", { sessionID: "ses_cancel", inputID: id }))
|
||||
send(durableEvent("session.inbox.delivered", { sessionID: "ses_cancel", inboxID: id }))
|
||||
},
|
||||
onInterrupt({ sessionID, send }) {
|
||||
send(durableEvent("session.execution.interrupted", { sessionID, reason: "user" }))
|
||||
@@ -680,7 +680,7 @@ describe("acp event behavior", () => {
|
||||
test("cancels unsupported session forms so execution can continue", async () => {
|
||||
const fixture = createSseFixture({
|
||||
onPrompt({ id, send }) {
|
||||
send(durableEvent("session.input.promoted", { sessionID: "ses_form", inputID: id }))
|
||||
send(durableEvent("session.inbox.delivered", { sessionID: "ses_form", inboxID: id }))
|
||||
send(
|
||||
ephemeralEvent("form.created", {
|
||||
form: {
|
||||
@@ -704,7 +704,7 @@ describe("acp event behavior", () => {
|
||||
fixture,
|
||||
connection: recordingConnection([]),
|
||||
sessionID: "ses_form",
|
||||
inputID: "input_form",
|
||||
inboxID: "input_form",
|
||||
})
|
||||
|
||||
expect(response.stopReason).toBe("end_turn")
|
||||
@@ -730,7 +730,7 @@ function turn(input: {
|
||||
readonly fixture: Fixture
|
||||
readonly connection: Connection
|
||||
readonly sessionID: string
|
||||
readonly inputID: string
|
||||
readonly inboxID: string
|
||||
readonly childSessionUpdate?: (update: ChildSessionUpdate) => Promise<void>
|
||||
}) {
|
||||
return streamTurn({
|
||||
@@ -738,12 +738,12 @@ function turn(input: {
|
||||
connection: input.connection,
|
||||
sessionID: input.sessionID,
|
||||
cwd: "/workspace",
|
||||
start: { type: "input", id: input.inputID },
|
||||
start: { type: "input", id: input.inboxID },
|
||||
writeTextFile: false,
|
||||
control: { cancelled: false, admission: new AbortController() },
|
||||
childSessionUpdate: input.childSessionUpdate,
|
||||
submit: (signal) =>
|
||||
input.fixture.client.session.prompt({ sessionID: input.sessionID, id: input.inputID, text: "hello" }, { signal }),
|
||||
input.fixture.client.session.prompt({ sessionID: input.sessionID, id: input.inboxID, text: "hello" }, { signal }),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -34,8 +34,8 @@ test("acp prompt resolves after ordered turn updates", async () => {
|
||||
send(events, {
|
||||
id: "evt_promoted",
|
||||
created: 1,
|
||||
type: "session.input.promoted",
|
||||
data: { sessionID: "ses_test", inputID: id },
|
||||
type: "session.inbox.delivered",
|
||||
data: { sessionID: "ses_test", inboxID: id },
|
||||
})
|
||||
send(events, {
|
||||
id: "evt_text",
|
||||
|
||||
@@ -38,7 +38,7 @@ describe("acp permission behavior", () => {
|
||||
const permissionRequests: RequestPermissionRequest[] = []
|
||||
const fixture = createSseFixture({
|
||||
onPrompt({ id, send }) {
|
||||
send(durableEvent("session.input.promoted", { sessionID: "ses_allow", inputID: id }))
|
||||
send(durableEvent("session.inbox.delivered", { sessionID: "ses_allow", inboxID: id }))
|
||||
send(
|
||||
permissionAsked("ses_allow", "perm_once", {
|
||||
action: "shell",
|
||||
@@ -112,7 +112,7 @@ describe("acp permission behavior", () => {
|
||||
const permissionRequests: RequestPermissionRequest[] = []
|
||||
const fixture = createSseFixture({
|
||||
onPrompt({ id, send }) {
|
||||
send(durableEvent("session.input.promoted", { sessionID: "ses_external", inputID: id }))
|
||||
send(durableEvent("session.inbox.delivered", { sessionID: "ses_external", inboxID: id }))
|
||||
send(
|
||||
permissionAsked("ses_external", "perm_external", {
|
||||
action: "external_directory",
|
||||
@@ -157,7 +157,7 @@ describe("acp permission behavior", () => {
|
||||
const permissionRequests: RequestPermissionRequest[] = []
|
||||
const fixture = createSseFixture({
|
||||
onPrompt({ id, send }) {
|
||||
send(durableEvent("session.input.promoted", { sessionID: "ses_parent", inputID: id }))
|
||||
send(durableEvent("session.inbox.delivered", { sessionID: "ses_parent", inboxID: id }))
|
||||
send(
|
||||
durableEvent("session.created", {
|
||||
sessionID: "ses_child",
|
||||
@@ -219,7 +219,7 @@ describe("acp permission behavior", () => {
|
||||
const writes: Parameters<AgentSideConnection["writeTextFile"]>[0][] = []
|
||||
const fixture = createSseFixture({
|
||||
onPrompt({ id, send }) {
|
||||
send(durableEvent("session.input.promoted", { sessionID: "ses_edit", inputID: id }))
|
||||
send(durableEvent("session.inbox.delivered", { sessionID: "ses_edit", inboxID: id }))
|
||||
send(
|
||||
durableEvent("session.tool.input.started", {
|
||||
sessionID: "ses_edit",
|
||||
@@ -309,7 +309,7 @@ describe("acp permission behavior", () => {
|
||||
const writes: Parameters<AgentSideConnection["writeTextFile"]>[0][] = []
|
||||
const fixture = createSseFixture({
|
||||
onPrompt({ id, send }) {
|
||||
send(durableEvent("session.input.promoted", { sessionID: "ses_patch", inputID: id }))
|
||||
send(durableEvent("session.inbox.delivered", { sessionID: "ses_patch", inboxID: id }))
|
||||
send(
|
||||
durableEvent("session.tool.input.started", {
|
||||
sessionID: "ses_patch",
|
||||
@@ -389,7 +389,7 @@ describe("acp permission behavior", () => {
|
||||
test("rejects explicit rejection, cancellation, and permission UI failure", async () => {
|
||||
const fixture = createSseFixture({
|
||||
onPrompt({ id, send }) {
|
||||
send(durableEvent("session.input.promoted", { sessionID: "ses_reject", inputID: id }))
|
||||
send(durableEvent("session.inbox.delivered", { sessionID: "ses_reject", inboxID: id }))
|
||||
send(permissionAsked("ses_reject", "perm_selected_reject"))
|
||||
send(permissionAsked("ses_reject", "perm_cancelled"))
|
||||
send(permissionAsked("ses_reject", "perm_failed"))
|
||||
@@ -426,7 +426,7 @@ describe("acp permission behavior", () => {
|
||||
const permissionRequests: RequestPermissionRequest[] = []
|
||||
const fixture = createSseFixture({
|
||||
onPrompt({ id, send }) {
|
||||
send(durableEvent("session.input.promoted", { sessionID: "ses_serial", inputID: id }))
|
||||
send(durableEvent("session.inbox.delivered", { sessionID: "ses_serial", inboxID: id }))
|
||||
send(permissionAsked("ses_serial", "perm_1"))
|
||||
send(permissionAsked("ses_serial", "perm_2"))
|
||||
send(durableEvent("session.execution.succeeded", { sessionID: "ses_serial" }))
|
||||
@@ -477,8 +477,8 @@ describe("acp permission behavior", () => {
|
||||
const blockedID = promptIDs.get("ses_blocked")
|
||||
const freeID = promptIDs.get("ses_free")
|
||||
if (!blockedID || !freeID) throw new Error("both permission test prompts must be registered")
|
||||
send(durableEvent("session.input.promoted", { sessionID: "ses_blocked", inputID: blockedID }))
|
||||
send(durableEvent("session.input.promoted", { sessionID: "ses_free", inputID: freeID }))
|
||||
send(durableEvent("session.inbox.delivered", { sessionID: "ses_blocked", inboxID: blockedID }))
|
||||
send(durableEvent("session.inbox.delivered", { sessionID: "ses_free", inboxID: freeID }))
|
||||
send(permissionAsked("ses_blocked", "perm_blocked"))
|
||||
send(
|
||||
ephemeralEvent("session.text.delta", {
|
||||
@@ -538,16 +538,16 @@ describe("acp permission behavior", () => {
|
||||
})
|
||||
})
|
||||
|
||||
function startTurn(fixture: Fixture, connection: Connection, sessionID: string, inputID: string, cwd = "/workspace") {
|
||||
function startTurn(fixture: Fixture, connection: Connection, sessionID: string, inboxID: string, cwd = "/workspace") {
|
||||
return streamTurn({
|
||||
client: fixture.client,
|
||||
connection,
|
||||
sessionID,
|
||||
cwd,
|
||||
start: { type: "input", id: inputID },
|
||||
start: { type: "input", id: inboxID },
|
||||
writeTextFile: true,
|
||||
control: { cancelled: false, admission: new AbortController() },
|
||||
submit: (signal) => fixture.client.session.prompt({ sessionID, id: inputID, text: "hello" }, { signal }),
|
||||
submit: (signal) => fixture.client.session.prompt({ sessionID, id: inboxID, text: "hello" }, { signal }),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -15,8 +15,8 @@ describe("acp service prompt routing and usage", () => {
|
||||
const id = requestID(request)
|
||||
completeTurn(context, "ses_routes", {
|
||||
id: `evt_${id}`,
|
||||
type: "session.input.promoted",
|
||||
data: { sessionID: "ses_routes", inputID: id },
|
||||
type: "session.inbox.delivered",
|
||||
data: { sessionID: "ses_routes", inboxID: id },
|
||||
})
|
||||
return Response.json({ data: {} })
|
||||
}
|
||||
@@ -33,8 +33,8 @@ describe("acp service prompt routing and usage", () => {
|
||||
const id = requestID(request)
|
||||
completeTurn(context, "ses_routes", {
|
||||
id: `evt_${id}`,
|
||||
type: "session.compaction.admitted",
|
||||
data: { sessionID: "ses_routes", inputID: id },
|
||||
type: "session.inbox.delivered",
|
||||
data: { sessionID: "ses_routes", inboxID: id },
|
||||
})
|
||||
return Response.json({ data: {} })
|
||||
}
|
||||
@@ -95,8 +95,8 @@ describe("acp service prompt routing and usage", () => {
|
||||
const id = requestID(request)
|
||||
context.send({
|
||||
id: `evt_${id}`,
|
||||
type: "session.input.promoted",
|
||||
data: { sessionID: "ses_usage", inputID: id },
|
||||
type: "session.inbox.delivered",
|
||||
data: { sessionID: "ses_usage", inboxID: id },
|
||||
})
|
||||
context.send({
|
||||
id: "evt_step",
|
||||
@@ -189,8 +189,8 @@ describe("acp service prompt routing and usage", () => {
|
||||
const id = requestID(request)
|
||||
context.send({
|
||||
id: `evt_${id}`,
|
||||
type: "session.input.promoted",
|
||||
data: { sessionID: "ses_usage_failure", inputID: id },
|
||||
type: "session.inbox.delivered",
|
||||
data: { sessionID: "ses_usage_failure", inboxID: id },
|
||||
})
|
||||
context.send({
|
||||
id: "evt_step_failure",
|
||||
|
||||
@@ -28,13 +28,13 @@ function formCreated(info: FormInfo, eventLocation = location): V2Event {
|
||||
return { id: `evt_${info.id}`, created: 0, type: "form.created", location: eventLocation, data: { form: info } }
|
||||
}
|
||||
|
||||
function prompted(inputID: string): V2Event {
|
||||
function prompted(inboxID: string): V2Event {
|
||||
return {
|
||||
id: "evt_prompted",
|
||||
created: 0,
|
||||
type: "session.input.promoted",
|
||||
type: "session.inbox.delivered",
|
||||
durable: { aggregateID: "ses_1", seq: 0, version: 1 },
|
||||
data: { sessionID: "ses_1", inputID },
|
||||
data: { sessionID: "ses_1", inboxID },
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,9 +98,9 @@ function executionFailed(message: string): V2Event {
|
||||
}
|
||||
}
|
||||
|
||||
function failedTool(inputID: string): V2Event[] {
|
||||
function failedTool(inboxID: string): V2Event[] {
|
||||
return [
|
||||
prompted(inputID),
|
||||
prompted(inboxID),
|
||||
{
|
||||
id: "evt_failed_tool_input",
|
||||
created: 1,
|
||||
@@ -156,10 +156,10 @@ function failedTool(inputID: string): V2Event[] {
|
||||
]
|
||||
}
|
||||
|
||||
function successfulGrep(inputID: string): V2Event[] {
|
||||
function successfulGrep(inboxID: string): V2Event[] {
|
||||
const text = "Found 2 matches\n/src/a.ts:\n Line 1: needle\n/src/b.ts:\n Line 2: needle"
|
||||
return [
|
||||
prompted(inputID),
|
||||
prompted(inboxID),
|
||||
{
|
||||
id: "evt_grep_input",
|
||||
created: 1,
|
||||
@@ -206,7 +206,7 @@ function successfulGrep(inputID: string): V2Event[] {
|
||||
// Runs one non-interactive prompt against a mocked SDK. `turn` produces the
|
||||
// live events the prompt admission triggers, keyed by the generated message ID.
|
||||
async function run(input: {
|
||||
turn: (inputID: string) => V2Event[]
|
||||
turn: (inboxID: string) => V2Event[]
|
||||
pendingForms?: FormInfo[]
|
||||
attached?: boolean
|
||||
format?: "default" | "json"
|
||||
@@ -214,7 +214,7 @@ async function run(input: {
|
||||
cancel?: (input: { sessionID: string; formID: string }) => Promise<void>
|
||||
renderTool?: (part: SessionMessageAssistantTool) => Promise<void>
|
||||
renderToolError?: (part: SessionMessageAssistantTool) => Promise<void>
|
||||
messages?: (inputID: string) => SessionMessageInfo[]
|
||||
messages?: (inboxID: string) => SessionMessageInfo[]
|
||||
wait?: () => Promise<void>
|
||||
terminalDelay?: number
|
||||
}) {
|
||||
@@ -515,7 +515,7 @@ describe("runNonInteractivePrompt", () => {
|
||||
const rendered: SessionMessageAssistantTool[] = []
|
||||
const failed: SessionMessageAssistantTool[] = []
|
||||
await capture({
|
||||
turn: (inputID) => failedTool(inputID).filter((event) => event.type !== "session.tool.progress"),
|
||||
turn: (inboxID) => failedTool(inboxID).filter((event) => event.type !== "session.tool.progress"),
|
||||
renderTool: (part) => {
|
||||
rendered.push(part)
|
||||
return Promise.resolve()
|
||||
|
||||
@@ -30,7 +30,7 @@ import { Reference } from "@opencode-ai/schema/reference"
|
||||
import { AbsolutePath, PositiveInt, RelativePath } from "@opencode-ai/schema/schema"
|
||||
import { Session } from "@opencode-ai/schema/session"
|
||||
import { SessionMessage } from "@opencode-ai/schema/session-message"
|
||||
import { SessionPending } from "@opencode-ai/schema/session-pending"
|
||||
import { SessionInbox } from "@opencode-ai/schema/session-inbox"
|
||||
import { Shell } from "@opencode-ai/schema/shell"
|
||||
import { Skill } from "@opencode-ai/schema/skill"
|
||||
import { Vcs } from "@opencode-ai/schema/vcs"
|
||||
@@ -69,7 +69,7 @@ const effectTypeReferences = [
|
||||
...namespaceTypes("Reference", "@opencode-ai/schema/reference", Reference),
|
||||
...namespaceTypes("Session", "@opencode-ai/schema/session", Session),
|
||||
...namespaceTypes("SessionMessage", "@opencode-ai/schema/session-message", SessionMessage),
|
||||
...namespaceTypes("SessionPending", "@opencode-ai/schema/session-pending", SessionPending),
|
||||
...namespaceTypes("SessionInbox", "@opencode-ai/schema/session-inbox", SessionInbox),
|
||||
...namespaceTypes("Shell", "@opencode-ai/schema/shell", Shell),
|
||||
...namespaceTypes("Skill", "@opencode-ai/schema/skill", Skill),
|
||||
...namespaceTypes("Vcs", "@opencode-ai/schema/vcs", Vcs),
|
||||
|
||||
@@ -11,9 +11,9 @@ import type { RelativePath } from "@opencode-ai/schema/schema"
|
||||
import type { Brand } from "effect"
|
||||
import type { Model } from "@opencode-ai/schema/model"
|
||||
import type { SessionMessage } from "@opencode-ai/schema/session-message"
|
||||
import type { SessionInbox } from "@opencode-ai/schema/session-inbox"
|
||||
import type { PromptInput } from "@opencode-ai/schema/prompt-input"
|
||||
import type { AgentAttachment } from "@opencode-ai/schema/prompt"
|
||||
import type { SessionPending } from "@opencode-ai/schema/session-pending"
|
||||
import type { Skill } from "@opencode-ai/schema/skill"
|
||||
import type { Event } from "@opencode-ai/schema/event"
|
||||
import type { InstructionEntry } from "@opencode-ai/schema/instruction-entry"
|
||||
@@ -170,6 +170,7 @@ export type Endpoint5_11Input = {
|
||||
readonly sessionID: Session.ID
|
||||
readonly directory: AbsolutePath
|
||||
readonly workspaceID?: Workspace.ID | undefined
|
||||
readonly delivery?: SessionInbox.Delivery | undefined
|
||||
}
|
||||
export type Endpoint5_11Output = void
|
||||
export type SessionMoveOperation<E = never> = (input: Endpoint5_11Input) => Effect.Effect<Endpoint5_11Output, E>
|
||||
@@ -182,10 +183,10 @@ export type Endpoint5_12Input = {
|
||||
readonly agents?: ReadonlyArray<AgentAttachment> | undefined
|
||||
readonly skills?: ReadonlyArray<PromptInput.SkillAttachment> | undefined
|
||||
readonly metadata?: { readonly [x: string]: unknown } | undefined
|
||||
readonly delivery?: "steer" | "queue" | undefined
|
||||
readonly delivery?: SessionInbox.Delivery | undefined
|
||||
readonly resume?: boolean | undefined
|
||||
}
|
||||
export type Endpoint5_12Output = SessionPending.User
|
||||
export type Endpoint5_12Output = SessionInbox.User
|
||||
export type SessionPromptOperation<E = never> = (input: Endpoint5_12Input) => Effect.Effect<Endpoint5_12Output, E>
|
||||
|
||||
export type Endpoint5_13Input = {
|
||||
@@ -198,10 +199,10 @@ export type Endpoint5_13Input = {
|
||||
readonly files?: ReadonlyArray<PromptInput.FileAttachment> | undefined
|
||||
readonly agents?: ReadonlyArray<AgentAttachment> | undefined
|
||||
readonly skills?: ReadonlyArray<PromptInput.SkillAttachment> | undefined
|
||||
readonly delivery?: "steer" | "queue" | undefined
|
||||
readonly delivery?: SessionInbox.Delivery | undefined
|
||||
readonly resume?: boolean | undefined
|
||||
}
|
||||
export type Endpoint5_13Output = SessionPending.User
|
||||
export type Endpoint5_13Output = SessionInbox.User
|
||||
export type SessionCommandOperation<E = never> = (input: Endpoint5_13Input) => Effect.Effect<Endpoint5_13Output, E>
|
||||
|
||||
export type Endpoint5_14Input = {
|
||||
@@ -219,10 +220,10 @@ export type Endpoint5_15Input = {
|
||||
readonly text: string
|
||||
readonly description?: string | undefined
|
||||
readonly metadata?: { readonly [x: string]: unknown } | undefined
|
||||
readonly delivery?: "steer" | "queue" | undefined
|
||||
readonly delivery?: SessionInbox.Delivery | undefined
|
||||
readonly resume?: boolean | undefined
|
||||
}
|
||||
export type Endpoint5_15Output = SessionPending.Synthetic
|
||||
export type Endpoint5_15Output = SessionInbox.Synthetic
|
||||
export type SessionSyntheticOperation<E = never> = (input: Endpoint5_15Input) => Effect.Effect<Endpoint5_15Output, E>
|
||||
|
||||
export type Endpoint5_16Input = {
|
||||
@@ -233,8 +234,12 @@ export type Endpoint5_16Input = {
|
||||
export type Endpoint5_16Output = void
|
||||
export type SessionShellOperation<E = never> = (input: Endpoint5_16Input) => Effect.Effect<Endpoint5_16Output, E>
|
||||
|
||||
export type Endpoint5_17Input = { readonly sessionID: Session.ID; readonly id?: SessionMessage.ID | undefined }
|
||||
export type Endpoint5_17Output = SessionPending.Compaction
|
||||
export type Endpoint5_17Input = {
|
||||
readonly sessionID: Session.ID
|
||||
readonly id?: SessionMessage.ID | undefined
|
||||
readonly delivery?: SessionInbox.Delivery | undefined
|
||||
}
|
||||
export type Endpoint5_17Output = SessionInbox.Compaction
|
||||
export type SessionCompactOperation<E = never> = (input: Endpoint5_17Input) => Effect.Effect<Endpoint5_17Output, E>
|
||||
|
||||
export type Endpoint5_18Input = { readonly sessionID: Session.ID }
|
||||
@@ -262,22 +267,20 @@ export type Endpoint5_22Output = ReadonlyArray<SessionMessage.Info>
|
||||
export type SessionContextOperation<E = never> = (input: Endpoint5_22Input) => Effect.Effect<Endpoint5_22Output, E>
|
||||
|
||||
export type Endpoint5_23Input = { readonly sessionID: Session.ID }
|
||||
export type Endpoint5_23Output = ReadonlyArray<SessionPending.Info>
|
||||
export type SessionPendingListOperation<E = never> = (input: Endpoint5_23Input) => Effect.Effect<Endpoint5_23Output, E>
|
||||
export type Endpoint5_23Output = ReadonlyArray<SessionInbox.Info>
|
||||
export type SessionInboxListOperation<E = never> = (input: Endpoint5_23Input) => Effect.Effect<Endpoint5_23Output, E>
|
||||
|
||||
export type Endpoint5_24Input = { readonly sessionID: Session.ID; readonly inputID: SessionMessage.ID }
|
||||
export type Endpoint5_24Input = { readonly sessionID: Session.ID; readonly inboxID: SessionMessage.ID }
|
||||
export type Endpoint5_24Output = void
|
||||
export type SessionPendingCancelOperation<E = never> = (
|
||||
input: Endpoint5_24Input,
|
||||
) => Effect.Effect<Endpoint5_24Output, E>
|
||||
export type SessionInboxCancelOperation<E = never> = (input: Endpoint5_24Input) => Effect.Effect<Endpoint5_24Output, E>
|
||||
|
||||
export type Endpoint5_25Input = { readonly sessionID: Session.ID; readonly inputID: SessionMessage.ID }
|
||||
export type Endpoint5_25Input = { readonly sessionID: Session.ID; readonly inboxID: SessionMessage.ID }
|
||||
export type Endpoint5_25Output = void
|
||||
export type SessionPendingSteerOperation<E = never> = (input: Endpoint5_25Input) => Effect.Effect<Endpoint5_25Output, E>
|
||||
export type SessionInboxSteerOperation<E = never> = (input: Endpoint5_25Input) => Effect.Effect<Endpoint5_25Output, E>
|
||||
|
||||
export type Endpoint5_26Input = { readonly sessionID: Session.ID; readonly inputID: SessionMessage.ID }
|
||||
export type Endpoint5_26Input = { readonly sessionID: Session.ID; readonly inboxID: SessionMessage.ID }
|
||||
export type Endpoint5_26Output = void
|
||||
export type SessionPendingQueueOperation<E = never> = (input: Endpoint5_26Input) => Effect.Effect<Endpoint5_26Output, E>
|
||||
export type SessionInboxQueueOperation<E = never> = (input: Endpoint5_26Input) => Effect.Effect<Endpoint5_26Output, E>
|
||||
|
||||
export type Endpoint5_27Input = { readonly sessionID: Session.ID }
|
||||
export type Endpoint5_27Output = ReadonlyArray<InstructionEntry.Info>
|
||||
@@ -368,7 +371,7 @@ export type Endpoint5_31Output =
|
||||
readonly data: {
|
||||
readonly sessionID: Session.ID
|
||||
readonly location: Location.Ref
|
||||
readonly projectID?: Project.ID | undefined
|
||||
readonly projectID: Project.ID
|
||||
readonly subpath?: RelativePath | undefined
|
||||
}
|
||||
}
|
||||
@@ -410,50 +413,45 @@ export type Endpoint5_31Output =
|
||||
readonly id: Event.ID
|
||||
readonly created: DateTime.Utc
|
||||
readonly metadata?: { readonly [x: string]: unknown } | undefined
|
||||
readonly type: "session.input.promoted"
|
||||
readonly type: "session.inbox.delivered"
|
||||
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
|
||||
readonly location?: Location.Ref | undefined
|
||||
readonly data: { readonly sessionID: Session.ID; readonly inputID: SessionMessage.ID }
|
||||
readonly data: { readonly sessionID: Session.ID; readonly inboxID: SessionMessage.ID }
|
||||
}
|
||||
| {
|
||||
readonly id: Event.ID
|
||||
readonly created: DateTime.Utc
|
||||
readonly metadata?: { readonly [x: string]: unknown } | undefined
|
||||
readonly type: "session.input.admitted"
|
||||
readonly type: "session.inbox.enqueued"
|
||||
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
|
||||
readonly location?: Location.Ref | undefined
|
||||
readonly data: {
|
||||
readonly sessionID: Session.ID
|
||||
readonly inputID: SessionMessage.ID
|
||||
readonly input: SessionPending.Message
|
||||
readonly inboxID: SessionMessage.ID
|
||||
readonly item: SessionInbox.Item
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: Event.ID
|
||||
readonly created: DateTime.Utc
|
||||
readonly metadata?: { readonly [x: string]: unknown } | undefined
|
||||
readonly type: "session.input.cancelled"
|
||||
readonly type: "session.inbox.cancelled"
|
||||
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
|
||||
readonly location?: Location.Ref | undefined
|
||||
readonly data: { readonly sessionID: Session.ID; readonly inputID: SessionMessage.ID }
|
||||
readonly data: { readonly sessionID: Session.ID; readonly inboxID: SessionMessage.ID }
|
||||
}
|
||||
| {
|
||||
readonly id: Event.ID
|
||||
readonly created: DateTime.Utc
|
||||
readonly metadata?: { readonly [x: string]: unknown } | undefined
|
||||
readonly type: "session.input.steered"
|
||||
readonly type: "session.inbox.delivery.changed"
|
||||
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
|
||||
readonly location?: Location.Ref | undefined
|
||||
readonly data: { readonly sessionID: Session.ID; readonly inputID: SessionMessage.ID }
|
||||
}
|
||||
| {
|
||||
readonly id: Event.ID
|
||||
readonly created: DateTime.Utc
|
||||
readonly metadata?: { readonly [x: string]: unknown } | undefined
|
||||
readonly type: "session.input.queued"
|
||||
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
|
||||
readonly location?: Location.Ref | undefined
|
||||
readonly data: { readonly sessionID: Session.ID; readonly inputID: SessionMessage.ID }
|
||||
readonly data: {
|
||||
readonly sessionID: Session.ID
|
||||
readonly inboxID: SessionMessage.ID
|
||||
readonly delivery: SessionInbox.Delivery
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: Event.ID
|
||||
@@ -814,15 +812,6 @@ export type Endpoint5_31Output =
|
||||
readonly error: { readonly type: string; readonly message: string; readonly status?: number | undefined }
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: Event.ID
|
||||
readonly created: DateTime.Utc
|
||||
readonly metadata?: { readonly [x: string]: unknown } | undefined
|
||||
readonly type: "session.compaction.admitted"
|
||||
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
|
||||
readonly location?: Location.Ref | undefined
|
||||
readonly data: { readonly sessionID: Session.ID; readonly inputID: SessionMessage.ID }
|
||||
}
|
||||
| {
|
||||
readonly id: Event.ID
|
||||
readonly created: DateTime.Utc
|
||||
@@ -953,11 +942,11 @@ export interface SessionApi<E = never> {
|
||||
readonly commit: SessionRevertCommitOperation<E>
|
||||
}
|
||||
readonly context: SessionContextOperation<E>
|
||||
readonly pending: {
|
||||
readonly list: SessionPendingListOperation<E>
|
||||
readonly cancel: SessionPendingCancelOperation<E>
|
||||
readonly steer: SessionPendingSteerOperation<E>
|
||||
readonly queue: SessionPendingQueueOperation<E>
|
||||
readonly inbox: {
|
||||
readonly list: SessionInboxListOperation<E>
|
||||
readonly cancel: SessionInboxCancelOperation<E>
|
||||
readonly steer: SessionInboxSteerOperation<E>
|
||||
readonly queue: SessionInboxQueueOperation<E>
|
||||
}
|
||||
readonly instructions: {
|
||||
readonly entry: {
|
||||
|
||||
@@ -399,7 +399,7 @@ const Endpoint5_11 = (raw: RawClient["server.session"]) => (input: Endpoint5_11I
|
||||
preserveEffect<Endpoint5_11Output>()(
|
||||
raw["session.move"]({
|
||||
params: { sessionID: input["sessionID"] },
|
||||
payload: { directory: input["directory"], workspaceID: input["workspaceID"] },
|
||||
payload: { directory: input["directory"], workspaceID: input["workspaceID"], delivery: input["delivery"] },
|
||||
}).pipe(Effect.mapError(mapClientError)),
|
||||
)
|
||||
|
||||
@@ -481,7 +481,10 @@ const Endpoint5_16 = (raw: RawClient["server.session"]) => (input: Endpoint5_16I
|
||||
|
||||
const Endpoint5_17 = (raw: RawClient["server.session"]) => (input: Endpoint5_17Input) =>
|
||||
preserveEffect<Endpoint5_17Output>()(
|
||||
raw["session.compact"]({ params: { sessionID: input["sessionID"] }, payload: { id: input["id"] } }).pipe(
|
||||
raw["session.compact"]({
|
||||
params: { sessionID: input["sessionID"] },
|
||||
payload: { id: input["id"], delivery: input["delivery"] },
|
||||
}).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
Effect.map((value) => value.data),
|
||||
),
|
||||
@@ -523,7 +526,7 @@ const Endpoint5_22 = (raw: RawClient["server.session"]) => (input: Endpoint5_22I
|
||||
|
||||
const Endpoint5_23 = (raw: RawClient["server.session"]) => (input: Endpoint5_23Input) =>
|
||||
preserveEffect<Endpoint5_23Output>()(
|
||||
raw["session.pending.list"]({ params: { sessionID: input["sessionID"] } }).pipe(
|
||||
raw["session.inbox.list"]({ params: { sessionID: input["sessionID"] } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
Effect.map((value) => value.data),
|
||||
),
|
||||
@@ -531,21 +534,21 @@ const Endpoint5_23 = (raw: RawClient["server.session"]) => (input: Endpoint5_23I
|
||||
|
||||
const Endpoint5_24 = (raw: RawClient["server.session"]) => (input: Endpoint5_24Input) =>
|
||||
preserveEffect<Endpoint5_24Output>()(
|
||||
raw["session.pending.cancel"]({ params: { sessionID: input["sessionID"], inputID: input["inputID"] } }).pipe(
|
||||
raw["session.inbox.cancel"]({ params: { sessionID: input["sessionID"], inboxID: input["inboxID"] } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
),
|
||||
)
|
||||
|
||||
const Endpoint5_25 = (raw: RawClient["server.session"]) => (input: Endpoint5_25Input) =>
|
||||
preserveEffect<Endpoint5_25Output>()(
|
||||
raw["session.pending.steer"]({ params: { sessionID: input["sessionID"], inputID: input["inputID"] } }).pipe(
|
||||
raw["session.inbox.steer"]({ params: { sessionID: input["sessionID"], inboxID: input["inboxID"] } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
),
|
||||
)
|
||||
|
||||
const Endpoint5_26 = (raw: RawClient["server.session"]) => (input: Endpoint5_26Input) =>
|
||||
preserveEffect<Endpoint5_26Output>()(
|
||||
raw["session.pending.queue"]({ params: { sessionID: input["sessionID"], inputID: input["inputID"] } }).pipe(
|
||||
raw["session.inbox.queue"]({ params: { sessionID: input["sessionID"], inboxID: input["inboxID"] } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
),
|
||||
)
|
||||
@@ -637,7 +640,7 @@ const adaptGroup5 = (raw: RawClient["server.session"]) => ({
|
||||
wait: Endpoint5_18(raw),
|
||||
revert: { stage: Endpoint5_19(raw), clear: Endpoint5_20(raw), commit: Endpoint5_21(raw) },
|
||||
context: Endpoint5_22(raw),
|
||||
pending: { list: Endpoint5_23(raw), cancel: Endpoint5_24(raw), steer: Endpoint5_25(raw), queue: Endpoint5_26(raw) },
|
||||
inbox: { list: Endpoint5_23(raw), cancel: Endpoint5_24(raw), steer: Endpoint5_25(raw), queue: Endpoint5_26(raw) },
|
||||
instructions: { entry: { list: Endpoint5_27(raw), put: Endpoint5_28(raw), remove: Endpoint5_29(raw) } },
|
||||
generate: Endpoint5_30(raw),
|
||||
log: Endpoint5_31(raw),
|
||||
|
||||
@@ -41,7 +41,7 @@ export { Reference } from "@opencode-ai/schema/reference"
|
||||
export { WebSearch } from "@opencode-ai/schema/websearch"
|
||||
export { AbsolutePath, RelativePath } from "@opencode-ai/schema/schema"
|
||||
export { Session } from "@opencode-ai/schema/session"
|
||||
export { SessionPending } from "@opencode-ai/schema/session-pending"
|
||||
export { SessionInbox } from "@opencode-ai/schema/session-inbox"
|
||||
export { SessionMessage } from "@opencode-ai/schema/session-message"
|
||||
export { Skill } from "@opencode-ai/schema/skill"
|
||||
export { Prompt } from "@opencode-ai/schema/prompt"
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
spawnServiceContender,
|
||||
} from "../service-contender.js"
|
||||
import { defaultEnsureTiming, ensureTiming, type EnsureTiming } from "../service-timing.js"
|
||||
import { matchesVersion } from "../service-version.js"
|
||||
|
||||
export * from "../service.js"
|
||||
/** Contents of the local service registration file. */
|
||||
@@ -37,14 +38,14 @@ export const incumbent = Effect.fn("service.incumbent")(function* (
|
||||
const info = yield* read(options.file)
|
||||
const found = info === undefined ? undefined : yield* probe({ ...info, url: options.url })
|
||||
if (found === undefined || found.legacy) return undefined
|
||||
if (options.version !== undefined && found.version !== options.version) return undefined
|
||||
if (!matchesVersion(found.version, options)) return undefined
|
||||
return { endpoint: found.endpoint, state: found.state }
|
||||
})
|
||||
|
||||
const discoverLocal = Effect.fnUntraced(function* (options: DiscoverOptions) {
|
||||
const found = (yield* registered(options.file)).service
|
||||
if (found?.state !== "ready") return undefined
|
||||
if (options.version !== undefined && found.version !== options.version) return undefined
|
||||
if (!matchesVersion(found.version, options)) return undefined
|
||||
return found
|
||||
})
|
||||
|
||||
@@ -93,7 +94,7 @@ export const ensure = Effect.fn("service.ensure")(function* (options: EnsureOpti
|
||||
} else timeouts = undefined
|
||||
if (service !== undefined) {
|
||||
spawnDelay = timing.spawnDelay
|
||||
const compatible = !service.legacy && (options.version === undefined || service.version === options.version)
|
||||
const compatible = !service.legacy && matchesVersion(service.version, options)
|
||||
if (compatible && service.state === "ready") return Option.some(service)
|
||||
if (compatible && service.state === "failed")
|
||||
return yield* Effect.fail(new Error("Background service failed to start"))
|
||||
|
||||
@@ -56,14 +56,14 @@ import type {
|
||||
SessionRevertCommitOutput,
|
||||
SessionContextInput,
|
||||
SessionContextOutput,
|
||||
SessionPendingListInput,
|
||||
SessionPendingListOutput,
|
||||
SessionPendingCancelInput,
|
||||
SessionPendingCancelOutput,
|
||||
SessionPendingSteerInput,
|
||||
SessionPendingSteerOutput,
|
||||
SessionPendingQueueInput,
|
||||
SessionPendingQueueOutput,
|
||||
SessionInboxListInput,
|
||||
SessionInboxListOutput,
|
||||
SessionInboxCancelInput,
|
||||
SessionInboxCancelOutput,
|
||||
SessionInboxSteerInput,
|
||||
SessionInboxSteerOutput,
|
||||
SessionInboxQueueInput,
|
||||
SessionInboxQueueOutput,
|
||||
SessionInstructionsEntryListInput,
|
||||
SessionInstructionsEntryListOutput,
|
||||
SessionInstructionsEntryPutInput,
|
||||
@@ -598,7 +598,7 @@ export function make(options: ClientOptions) {
|
||||
{
|
||||
method: "POST",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/move`,
|
||||
body: { directory: input["directory"], workspaceID: input["workspaceID"] },
|
||||
body: { directory: input["directory"], workspaceID: input["workspaceID"], delivery: input["delivery"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [404, 400, 401],
|
||||
empty: true,
|
||||
@@ -697,7 +697,7 @@ export function make(options: ClientOptions) {
|
||||
{
|
||||
method: "POST",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/compact`,
|
||||
body: { id: input["id"] },
|
||||
body: { id: input["id"], delivery: input["delivery"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [409, 404, 400, 401],
|
||||
empty: false,
|
||||
@@ -762,45 +762,45 @@ export function make(options: ClientOptions) {
|
||||
},
|
||||
requestOptions,
|
||||
).then((value) => value.data),
|
||||
pending: {
|
||||
list: (input: SessionPendingListInput, requestOptions?: RequestOptions) =>
|
||||
request<{ readonly data: SessionPendingListOutput }>(
|
||||
inbox: {
|
||||
list: (input: SessionInboxListInput, requestOptions?: RequestOptions) =>
|
||||
request<{ readonly data: SessionInboxListOutput }>(
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/pending`,
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/inbox`,
|
||||
successStatus: 200,
|
||||
declaredStatuses: [404, 401, 400],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
).then((value) => value.data),
|
||||
cancel: (input: SessionPendingCancelInput, requestOptions?: RequestOptions) =>
|
||||
request<SessionPendingCancelOutput>(
|
||||
cancel: (input: SessionInboxCancelInput, requestOptions?: RequestOptions) =>
|
||||
request<SessionInboxCancelOutput>(
|
||||
{
|
||||
method: "DELETE",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/pending/${encodeURIComponent(input.inputID)}`,
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/inbox/${encodeURIComponent(input.inboxID)}`,
|
||||
successStatus: 204,
|
||||
declaredStatuses: [409, 404, 401, 400],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
steer: (input: SessionPendingSteerInput, requestOptions?: RequestOptions) =>
|
||||
request<SessionPendingSteerOutput>(
|
||||
steer: (input: SessionInboxSteerInput, requestOptions?: RequestOptions) =>
|
||||
request<SessionInboxSteerOutput>(
|
||||
{
|
||||
method: "POST",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/pending/${encodeURIComponent(input.inputID)}/steer`,
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/inbox/${encodeURIComponent(input.inboxID)}/steer`,
|
||||
successStatus: 204,
|
||||
declaredStatuses: [409, 404, 401, 400],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
queue: (input: SessionPendingQueueInput, requestOptions?: RequestOptions) =>
|
||||
request<SessionPendingQueueOutput>(
|
||||
queue: (input: SessionInboxQueueInput, requestOptions?: RequestOptions) =>
|
||||
request<SessionInboxQueueOutput>(
|
||||
{
|
||||
method: "POST",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/pending/${encodeURIComponent(input.inputID)}/queue`,
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/inbox/${encodeURIComponent(input.inboxID)}/queue`,
|
||||
successStatus: 204,
|
||||
declaredStatuses: [409, 404, 401, 400],
|
||||
empty: true,
|
||||
|
||||
@@ -130,15 +130,17 @@ export type SessionMessageCompactionCompleted = {
|
||||
|
||||
export type SessionActive = { type: "running" }
|
||||
|
||||
export type SessionPendingSyntheticData = { text: string; description?: string; metadata?: { [x: string]: JsonValue } }
|
||||
export type SessionInboxDelivery = "steer" | "queue"
|
||||
|
||||
export type SessionPendingCompaction = { id: string; sessionID: string; timeCreated: number; type: "compaction" }
|
||||
export type SessionInboxSyntheticPayload = { text: string; description?: string; metadata?: { [x: string]: JsonValue } }
|
||||
|
||||
export type SessionInboxCompactionPayload = {}
|
||||
|
||||
export type InstructionEntryKey = string
|
||||
|
||||
export type SessionGenerateResponse = { data: { text: string } }
|
||||
|
||||
export type SessionPendingSyntheticData1 = { text: string; description?: string; metadata?: { [x: string]: any } }
|
||||
export type SessionInboxSyntheticPayload1 = { text: string; description?: string; metadata?: { [x: string]: any } }
|
||||
|
||||
export type ShellInfo = {
|
||||
id: string
|
||||
@@ -420,6 +422,8 @@ export type SessionMessageLocationSwitched = {
|
||||
previous?: { location: LocationRef; projectID?: string; subpath?: string }
|
||||
}
|
||||
|
||||
export type SessionInboxMovePayload = { location: LocationRef; projectID: string; subpath?: string }
|
||||
|
||||
export type SessionCreated = {
|
||||
id: string
|
||||
created: number
|
||||
@@ -468,7 +472,7 @@ export type SessionMoved = {
|
||||
type: "session.moved"
|
||||
durable: { aggregateID: string; seq: number; version: 1 }
|
||||
location?: LocationRef
|
||||
data: { sessionID: string; location: LocationRef; projectID?: string; subpath?: string }
|
||||
data: { sessionID: string; location: LocationRef; projectID: string; subpath?: string }
|
||||
}
|
||||
|
||||
export type SessionRenamed = {
|
||||
@@ -501,44 +505,24 @@ export type SessionForked = {
|
||||
data: { sessionID: string; parentID: string; boundary: SessionForkBoundary; instructions?: { [x: string]: string } }
|
||||
}
|
||||
|
||||
export type SessionInputPromoted = {
|
||||
export type SessionInboxDelivered = {
|
||||
id: string
|
||||
created: number
|
||||
metadata?: { [x: string]: any }
|
||||
type: "session.input.promoted"
|
||||
type: "session.inbox.delivered"
|
||||
durable: { aggregateID: string; seq: number; version: 1 }
|
||||
location?: LocationRef
|
||||
data: { sessionID: string; inputID: string }
|
||||
data: { sessionID: string; inboxID: string }
|
||||
}
|
||||
|
||||
export type SessionInputCancelled = {
|
||||
export type SessionInboxCancelled = {
|
||||
id: string
|
||||
created: number
|
||||
metadata?: { [x: string]: any }
|
||||
type: "session.input.cancelled"
|
||||
type: "session.inbox.cancelled"
|
||||
durable: { aggregateID: string; seq: number; version: 1 }
|
||||
location?: LocationRef
|
||||
data: { sessionID: string; inputID: string }
|
||||
}
|
||||
|
||||
export type SessionInputSteered = {
|
||||
id: string
|
||||
created: number
|
||||
metadata?: { [x: string]: any }
|
||||
type: "session.input.steered"
|
||||
durable: { aggregateID: string; seq: number; version: 1 }
|
||||
location?: LocationRef
|
||||
data: { sessionID: string; inputID: string }
|
||||
}
|
||||
|
||||
export type SessionInputQueued = {
|
||||
id: string
|
||||
created: number
|
||||
metadata?: { [x: string]: any }
|
||||
type: "session.input.queued"
|
||||
durable: { aggregateID: string; seq: number; version: 1 }
|
||||
location?: LocationRef
|
||||
data: { sessionID: string; inputID: string }
|
||||
data: { sessionID: string; inboxID: string }
|
||||
}
|
||||
|
||||
export type SessionExecutionStarted = {
|
||||
@@ -659,16 +643,6 @@ export type SessionToolInputEnded = {
|
||||
data: { sessionID: string; assistantMessageID: string; id: string; text: string }
|
||||
}
|
||||
|
||||
export type SessionCompactionAdmitted = {
|
||||
id: string
|
||||
created: number
|
||||
metadata?: { [x: string]: any }
|
||||
type: "session.compaction.admitted"
|
||||
durable: { aggregateID: string; seq: number; version: 1 }
|
||||
location?: LocationRef
|
||||
data: { sessionID: string; inputID: string }
|
||||
}
|
||||
|
||||
export type SessionCompactionStarted = {
|
||||
id: string
|
||||
created: number
|
||||
@@ -863,6 +837,16 @@ export type ProjectDirectoriesUpdated = {
|
||||
data: { projectID: string }
|
||||
}
|
||||
|
||||
export type ProjectDirectoryResolved = {
|
||||
id: string
|
||||
created: number
|
||||
metadata?: { [x: string]: any }
|
||||
type: "project.directory.resolved"
|
||||
durable: { aggregateID: string; seq: number; version: 1 }
|
||||
location?: LocationRef
|
||||
data: { projectID: string; directory: string; previous: string }
|
||||
}
|
||||
|
||||
export type CommandUpdated = {
|
||||
id: string
|
||||
created: number
|
||||
@@ -1162,23 +1146,36 @@ export type SessionCompactionFailed = {
|
||||
data: { sessionID: string; reason: "auto" | "manual"; error: SessionStructuredError; inputID?: string }
|
||||
}
|
||||
|
||||
export type SessionPendingSynthetic = {
|
||||
export type SessionInboxDeliveryChanged = {
|
||||
id: string
|
||||
created: number
|
||||
metadata?: { [x: string]: any }
|
||||
type: "session.inbox.delivery.changed"
|
||||
durable: { aggregateID: string; seq: number; version: 1 }
|
||||
location?: LocationRef
|
||||
data: { sessionID: string; inboxID: string; delivery: SessionInboxDelivery }
|
||||
}
|
||||
|
||||
export type SessionInboxSynthetic = {
|
||||
id: string
|
||||
sessionID: string
|
||||
timeCreated: number
|
||||
type: "synthetic"
|
||||
data: SessionPendingSyntheticData
|
||||
delivery: "steer" | "queue"
|
||||
payload: SessionInboxSyntheticPayload
|
||||
delivery: SessionInboxDelivery
|
||||
}
|
||||
|
||||
export type SessionInboxCompaction = {
|
||||
id: string
|
||||
sessionID: string
|
||||
timeCreated: number
|
||||
type: "compaction"
|
||||
payload: SessionInboxCompactionPayload
|
||||
delivery: SessionInboxDelivery
|
||||
}
|
||||
|
||||
export type InstructionEntryInfo = { key: InstructionEntryKey; value: JsonValue }
|
||||
|
||||
export type SessionPendingSyntheticMessage = {
|
||||
type: "synthetic"
|
||||
data: SessionPendingSyntheticData1
|
||||
delivery: "steer" | "queue"
|
||||
}
|
||||
|
||||
export type SessionShellStarted = {
|
||||
id: string
|
||||
created: number
|
||||
@@ -1527,6 +1524,15 @@ export type VcsInfo = { branch: VcsBranch }
|
||||
|
||||
export type PermissionRuleset = Array<PermissionRule>
|
||||
|
||||
export type SessionInboxMove = {
|
||||
id: string
|
||||
sessionID: string
|
||||
timeCreated: number
|
||||
type: "move"
|
||||
payload: SessionInboxMovePayload
|
||||
delivery: SessionInboxDelivery
|
||||
}
|
||||
|
||||
export type SessionInfo = {
|
||||
id: string
|
||||
parentID?: string
|
||||
@@ -1564,7 +1570,7 @@ export type SessionMessageUser = {
|
||||
type: "user"
|
||||
}
|
||||
|
||||
export type SessionPendingUserData = {
|
||||
export type SessionInboxUserPayload = {
|
||||
text: string
|
||||
files?: Array<PromptFileAttachment>
|
||||
agents?: Array<PromptAgentAttachment>
|
||||
@@ -1572,7 +1578,7 @@ export type SessionPendingUserData = {
|
||||
metadata?: { [x: string]: JsonValue }
|
||||
}
|
||||
|
||||
export type SessionPendingUserData1 = {
|
||||
export type SessionInboxUserPayload1 = {
|
||||
text: string
|
||||
files?: Array<PromptFileAttachment>
|
||||
agents?: Array<PromptAgentAttachment>
|
||||
@@ -1884,16 +1890,20 @@ export type ConfigEntry =
|
||||
|
||||
export type SessionsResponse = { data: Array<SessionInfo>; cursor: { previous?: string | null; next?: string | null } }
|
||||
|
||||
export type SessionPendingUser = {
|
||||
export type SessionInboxUser = {
|
||||
id: string
|
||||
sessionID: string
|
||||
timeCreated: number
|
||||
type: "user"
|
||||
data: SessionPendingUserData
|
||||
delivery: "steer" | "queue"
|
||||
payload: SessionInboxUserPayload
|
||||
delivery: SessionInboxDelivery
|
||||
}
|
||||
|
||||
export type SessionPendingUserMessage = { type: "user"; data: SessionPendingUserData1; delivery: "steer" | "queue" }
|
||||
export type SessionInboxItem =
|
||||
| { type: "user"; payload: SessionInboxUserPayload1; delivery: SessionInboxDelivery }
|
||||
| { type: "synthetic"; payload: SessionInboxSyntheticPayload1; delivery: SessionInboxDelivery }
|
||||
| { type: "compaction"; payload: SessionInboxCompactionPayload; delivery: SessionInboxDelivery }
|
||||
| { type: "move"; payload: SessionInboxMovePayload; delivery: SessionInboxDelivery }
|
||||
|
||||
export type SessionMessageAssistantTool = {
|
||||
type: "tool"
|
||||
@@ -1914,9 +1924,17 @@ export type FormFields = [FormField, ...Array<FormField>]
|
||||
|
||||
export type FormFields3 = [FormField1, ...Array<FormField1>]
|
||||
|
||||
export type SessionPendingInfo = SessionPendingUser | SessionPendingSynthetic | SessionPendingCompaction
|
||||
export type SessionInboxInfo = SessionInboxUser | SessionInboxSynthetic | SessionInboxCompaction | SessionInboxMove
|
||||
|
||||
export type SessionPendingMessage = SessionPendingUserMessage | SessionPendingSyntheticMessage
|
||||
export type SessionInboxEnqueued = {
|
||||
id: string
|
||||
created: number
|
||||
metadata?: { [x: string]: any }
|
||||
type: "session.inbox.enqueued"
|
||||
durable: { aggregateID: string; seq: number; version: 1 }
|
||||
location?: LocationRef
|
||||
data: { sessionID: string; inboxID: string; item: SessionInboxItem }
|
||||
}
|
||||
|
||||
export type SessionMessageAssistant = {
|
||||
id: string
|
||||
@@ -1942,15 +1960,47 @@ export type FormInfo = { id: string; sessionID: string; title: string; metadata?
|
||||
|
||||
export type FormInfo1 = { id: string; sessionID: string; title: string; metadata?: FormMetadata1; fields: FormFields3 }
|
||||
|
||||
export type SessionInputAdmitted = {
|
||||
id: string
|
||||
created: number
|
||||
metadata?: { [x: string]: any }
|
||||
type: "session.input.admitted"
|
||||
durable: { aggregateID: string; seq: number; version: 1 }
|
||||
location?: LocationRef
|
||||
data: { sessionID: string; inputID: string; input: SessionPendingMessage }
|
||||
}
|
||||
export type SessionEventDurable =
|
||||
| SessionCreated
|
||||
| SessionAgentSelected
|
||||
| SessionModelSelected
|
||||
| SessionMoved
|
||||
| SessionRenamed
|
||||
| SessionDeleted
|
||||
| SessionForked
|
||||
| SessionInboxDelivered
|
||||
| SessionInboxEnqueued
|
||||
| SessionInboxCancelled
|
||||
| SessionInboxDeliveryChanged
|
||||
| SessionExecutionStarted
|
||||
| SessionExecutionSucceeded
|
||||
| SessionExecutionFailed
|
||||
| SessionExecutionInterrupted
|
||||
| SessionInstructionsUpdated
|
||||
| SessionSynthetic
|
||||
| SessionSkillActivated
|
||||
| SessionShellStarted
|
||||
| SessionShellEnded
|
||||
| SessionStepStarted
|
||||
| SessionStepEnded
|
||||
| SessionStepFailed
|
||||
| SessionTextStarted
|
||||
| SessionTextEnded
|
||||
| SessionReasoningStarted
|
||||
| SessionReasoningEnded
|
||||
| SessionToolInputStarted
|
||||
| SessionToolInputEnded
|
||||
| SessionToolCalled
|
||||
| SessionToolSuccess
|
||||
| SessionToolFailed
|
||||
| SessionRetryScheduled
|
||||
| SessionCompactionStarted
|
||||
| SessionCompactionEnded
|
||||
| SessionCompactionFailed
|
||||
| SessionRevertStaged
|
||||
| SessionRevertCleared
|
||||
| SessionRevertCommitted
|
||||
| SessionUsageRecorded
|
||||
|
||||
export type SessionMessageInfo =
|
||||
| SessionMessageAgentSelected
|
||||
@@ -1979,49 +2029,7 @@ export type FormCreated = {
|
||||
data: { form: FormInfo1 }
|
||||
}
|
||||
|
||||
export type SessionEventDurable =
|
||||
| SessionCreated
|
||||
| SessionAgentSelected
|
||||
| SessionModelSelected
|
||||
| SessionMoved
|
||||
| SessionRenamed
|
||||
| SessionDeleted
|
||||
| SessionForked
|
||||
| SessionInputPromoted
|
||||
| SessionInputAdmitted
|
||||
| SessionInputCancelled
|
||||
| SessionInputSteered
|
||||
| SessionInputQueued
|
||||
| SessionExecutionStarted
|
||||
| SessionExecutionSucceeded
|
||||
| SessionExecutionFailed
|
||||
| SessionExecutionInterrupted
|
||||
| SessionInstructionsUpdated
|
||||
| SessionSynthetic
|
||||
| SessionSkillActivated
|
||||
| SessionShellStarted
|
||||
| SessionShellEnded
|
||||
| SessionStepStarted
|
||||
| SessionStepEnded
|
||||
| SessionStepFailed
|
||||
| SessionTextStarted
|
||||
| SessionTextEnded
|
||||
| SessionReasoningStarted
|
||||
| SessionReasoningEnded
|
||||
| SessionToolInputStarted
|
||||
| SessionToolInputEnded
|
||||
| SessionToolCalled
|
||||
| SessionToolSuccess
|
||||
| SessionToolFailed
|
||||
| SessionRetryScheduled
|
||||
| SessionCompactionAdmitted
|
||||
| SessionCompactionStarted
|
||||
| SessionCompactionEnded
|
||||
| SessionCompactionFailed
|
||||
| SessionRevertStaged
|
||||
| SessionRevertCleared
|
||||
| SessionRevertCommitted
|
||||
| SessionUsageRecorded
|
||||
export type SessionLogItem = SessionEventDurable | EventLogSynced
|
||||
|
||||
export type SessionTransferData = { info: SessionInfo; messages: Array<SessionMessageInfo> }
|
||||
|
||||
@@ -2051,11 +2059,10 @@ export type V2Event =
|
||||
| SessionUsageUpdated
|
||||
| SessionDeleted
|
||||
| SessionForked
|
||||
| SessionInputPromoted
|
||||
| SessionInputAdmitted
|
||||
| SessionInputCancelled
|
||||
| SessionInputSteered
|
||||
| SessionInputQueued
|
||||
| SessionInboxDelivered
|
||||
| SessionInboxEnqueued
|
||||
| SessionInboxCancelled
|
||||
| SessionInboxDeliveryChanged
|
||||
| SessionExecutionStarted
|
||||
| SessionExecutionSucceeded
|
||||
| SessionExecutionFailed
|
||||
@@ -2082,7 +2089,6 @@ export type V2Event =
|
||||
| SessionToolSuccess
|
||||
| SessionToolFailed
|
||||
| SessionRetryScheduled
|
||||
| SessionCompactionAdmitted
|
||||
| SessionCompactionStarted
|
||||
| SessionCompactionDelta
|
||||
| SessionCompactionEnded
|
||||
@@ -2097,6 +2103,7 @@ export type V2Event =
|
||||
| PluginAdded
|
||||
| PluginUpdated
|
||||
| ProjectDirectoriesUpdated
|
||||
| ProjectDirectoryResolved
|
||||
| CommandUpdated
|
||||
| ConfigUpdated
|
||||
| SkillUpdated
|
||||
@@ -2127,8 +2134,6 @@ export type V2Event =
|
||||
| McpResourcesChanged
|
||||
| V2EventServerConnected
|
||||
|
||||
export type SessionLogItem = SessionEventDurable | EventLogSynced
|
||||
|
||||
export type UnauthorizedError = { readonly _tag: "UnauthorizedError"; readonly message: string }
|
||||
export const isUnauthorizedError = (value: unknown): value is UnauthorizedError =>
|
||||
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "UnauthorizedError"
|
||||
@@ -3361,8 +3366,21 @@ export type SessionRenameOutput = void
|
||||
|
||||
export type SessionMoveInput = {
|
||||
readonly sessionID: { readonly sessionID: string }["sessionID"]
|
||||
readonly directory: { readonly directory: string; readonly workspaceID?: string }["directory"]
|
||||
readonly workspaceID?: { readonly directory: string; readonly workspaceID?: string }["workspaceID"]
|
||||
readonly directory: {
|
||||
readonly directory: string
|
||||
readonly workspaceID?: string
|
||||
readonly delivery?: ("steer" | "queue") | null
|
||||
}["directory"]
|
||||
readonly workspaceID?: {
|
||||
readonly directory: string
|
||||
readonly workspaceID?: string
|
||||
readonly delivery?: ("steer" | "queue") | null
|
||||
}["workspaceID"]
|
||||
readonly delivery?: {
|
||||
readonly directory: string
|
||||
readonly workspaceID?: string
|
||||
readonly delivery?: ("steer" | "queue") | null
|
||||
}["delivery"]
|
||||
}
|
||||
|
||||
export type SessionMoveOutput = void
|
||||
@@ -3387,7 +3405,7 @@ export type SessionPromptInput = {
|
||||
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly delivery?: "steer" | "queue" | null
|
||||
readonly delivery?: ("steer" | "queue") | null
|
||||
readonly resume?: boolean | null
|
||||
}["id"]
|
||||
readonly text: {
|
||||
@@ -3408,7 +3426,7 @@ export type SessionPromptInput = {
|
||||
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly delivery?: "steer" | "queue" | null
|
||||
readonly delivery?: ("steer" | "queue") | null
|
||||
readonly resume?: boolean | null
|
||||
}["text"]
|
||||
readonly files?: {
|
||||
@@ -3429,7 +3447,7 @@ export type SessionPromptInput = {
|
||||
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly delivery?: "steer" | "queue" | null
|
||||
readonly delivery?: ("steer" | "queue") | null
|
||||
readonly resume?: boolean | null
|
||||
}["files"]
|
||||
readonly agents?: {
|
||||
@@ -3450,7 +3468,7 @@ export type SessionPromptInput = {
|
||||
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly delivery?: "steer" | "queue" | null
|
||||
readonly delivery?: ("steer" | "queue") | null
|
||||
readonly resume?: boolean | null
|
||||
}["agents"]
|
||||
readonly skills?: {
|
||||
@@ -3471,7 +3489,7 @@ export type SessionPromptInput = {
|
||||
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly delivery?: "steer" | "queue" | null
|
||||
readonly delivery?: ("steer" | "queue") | null
|
||||
readonly resume?: boolean | null
|
||||
}["skills"]
|
||||
readonly metadata?: {
|
||||
@@ -3492,7 +3510,7 @@ export type SessionPromptInput = {
|
||||
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly delivery?: "steer" | "queue" | null
|
||||
readonly delivery?: ("steer" | "queue") | null
|
||||
readonly resume?: boolean | null
|
||||
}["metadata"]
|
||||
readonly delivery?: {
|
||||
@@ -3513,7 +3531,7 @@ export type SessionPromptInput = {
|
||||
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly delivery?: "steer" | "queue" | null
|
||||
readonly delivery?: ("steer" | "queue") | null
|
||||
readonly resume?: boolean | null
|
||||
}["delivery"]
|
||||
readonly resume?: {
|
||||
@@ -3534,12 +3552,12 @@ export type SessionPromptInput = {
|
||||
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly delivery?: "steer" | "queue" | null
|
||||
readonly delivery?: ("steer" | "queue") | null
|
||||
readonly resume?: boolean | null
|
||||
}["resume"]
|
||||
}
|
||||
|
||||
export type SessionPromptOutput = { data: SessionPendingUser }["data"]
|
||||
export type SessionPromptOutput = { data: SessionInboxUser }["data"]
|
||||
|
||||
export type SessionCommandInput = {
|
||||
readonly sessionID: { readonly sessionID: string }["sessionID"]
|
||||
@@ -3563,7 +3581,7 @@ export type SessionCommandInput = {
|
||||
readonly id: string
|
||||
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
readonly delivery?: "steer" | "queue" | null
|
||||
readonly delivery?: ("steer" | "queue") | null
|
||||
readonly resume?: boolean | null
|
||||
}["id"]
|
||||
readonly command: {
|
||||
@@ -3586,7 +3604,7 @@ export type SessionCommandInput = {
|
||||
readonly id: string
|
||||
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
readonly delivery?: "steer" | "queue" | null
|
||||
readonly delivery?: ("steer" | "queue") | null
|
||||
readonly resume?: boolean | null
|
||||
}["command"]
|
||||
readonly arguments?: {
|
||||
@@ -3609,7 +3627,7 @@ export type SessionCommandInput = {
|
||||
readonly id: string
|
||||
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
readonly delivery?: "steer" | "queue" | null
|
||||
readonly delivery?: ("steer" | "queue") | null
|
||||
readonly resume?: boolean | null
|
||||
}["arguments"]
|
||||
readonly agent?: {
|
||||
@@ -3632,7 +3650,7 @@ export type SessionCommandInput = {
|
||||
readonly id: string
|
||||
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
readonly delivery?: "steer" | "queue" | null
|
||||
readonly delivery?: ("steer" | "queue") | null
|
||||
readonly resume?: boolean | null
|
||||
}["agent"]
|
||||
readonly model?: {
|
||||
@@ -3655,7 +3673,7 @@ export type SessionCommandInput = {
|
||||
readonly id: string
|
||||
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
readonly delivery?: "steer" | "queue" | null
|
||||
readonly delivery?: ("steer" | "queue") | null
|
||||
readonly resume?: boolean | null
|
||||
}["model"]
|
||||
readonly files?: {
|
||||
@@ -3678,7 +3696,7 @@ export type SessionCommandInput = {
|
||||
readonly id: string
|
||||
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
readonly delivery?: "steer" | "queue" | null
|
||||
readonly delivery?: ("steer" | "queue") | null
|
||||
readonly resume?: boolean | null
|
||||
}["files"]
|
||||
readonly agents?: {
|
||||
@@ -3701,7 +3719,7 @@ export type SessionCommandInput = {
|
||||
readonly id: string
|
||||
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
readonly delivery?: "steer" | "queue" | null
|
||||
readonly delivery?: ("steer" | "queue") | null
|
||||
readonly resume?: boolean | null
|
||||
}["agents"]
|
||||
readonly skills?: {
|
||||
@@ -3724,7 +3742,7 @@ export type SessionCommandInput = {
|
||||
readonly id: string
|
||||
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
readonly delivery?: "steer" | "queue" | null
|
||||
readonly delivery?: ("steer" | "queue") | null
|
||||
readonly resume?: boolean | null
|
||||
}["skills"]
|
||||
readonly delivery?: {
|
||||
@@ -3747,7 +3765,7 @@ export type SessionCommandInput = {
|
||||
readonly id: string
|
||||
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
readonly delivery?: "steer" | "queue" | null
|
||||
readonly delivery?: ("steer" | "queue") | null
|
||||
readonly resume?: boolean | null
|
||||
}["delivery"]
|
||||
readonly resume?: {
|
||||
@@ -3770,12 +3788,12 @@ export type SessionCommandInput = {
|
||||
readonly id: string
|
||||
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
readonly delivery?: "steer" | "queue" | null
|
||||
readonly delivery?: ("steer" | "queue") | null
|
||||
readonly resume?: boolean | null
|
||||
}["resume"]
|
||||
}
|
||||
|
||||
export type SessionCommandOutput = { data: SessionPendingUser }["data"]
|
||||
export type SessionCommandOutput = { data: SessionInboxUser }["data"]
|
||||
|
||||
export type SessionSkillInput = {
|
||||
readonly sessionID: { readonly sessionID: string }["sessionID"]
|
||||
@@ -3805,7 +3823,7 @@ export type SessionSyntheticInput = {
|
||||
readonly text: string
|
||||
readonly description?: string | null
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly delivery?: "steer" | "queue" | null
|
||||
readonly delivery?: ("steer" | "queue") | null
|
||||
readonly resume?: boolean | null
|
||||
}["id"]
|
||||
readonly text: {
|
||||
@@ -3813,7 +3831,7 @@ export type SessionSyntheticInput = {
|
||||
readonly text: string
|
||||
readonly description?: string | null
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly delivery?: "steer" | "queue" | null
|
||||
readonly delivery?: ("steer" | "queue") | null
|
||||
readonly resume?: boolean | null
|
||||
}["text"]
|
||||
readonly description?: {
|
||||
@@ -3821,7 +3839,7 @@ export type SessionSyntheticInput = {
|
||||
readonly text: string
|
||||
readonly description?: string | null
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly delivery?: "steer" | "queue" | null
|
||||
readonly delivery?: ("steer" | "queue") | null
|
||||
readonly resume?: boolean | null
|
||||
}["description"]
|
||||
readonly metadata?: {
|
||||
@@ -3829,7 +3847,7 @@ export type SessionSyntheticInput = {
|
||||
readonly text: string
|
||||
readonly description?: string | null
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly delivery?: "steer" | "queue" | null
|
||||
readonly delivery?: ("steer" | "queue") | null
|
||||
readonly resume?: boolean | null
|
||||
}["metadata"]
|
||||
readonly delivery?: {
|
||||
@@ -3837,7 +3855,7 @@ export type SessionSyntheticInput = {
|
||||
readonly text: string
|
||||
readonly description?: string | null
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly delivery?: "steer" | "queue" | null
|
||||
readonly delivery?: ("steer" | "queue") | null
|
||||
readonly resume?: boolean | null
|
||||
}["delivery"]
|
||||
readonly resume?: {
|
||||
@@ -3845,12 +3863,12 @@ export type SessionSyntheticInput = {
|
||||
readonly text: string
|
||||
readonly description?: string | null
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly delivery?: "steer" | "queue" | null
|
||||
readonly delivery?: ("steer" | "queue") | null
|
||||
readonly resume?: boolean | null
|
||||
}["resume"]
|
||||
}
|
||||
|
||||
export type SessionSyntheticOutput = { data: SessionPendingSynthetic }["data"]
|
||||
export type SessionSyntheticOutput = { data: SessionInboxSynthetic }["data"]
|
||||
|
||||
export type SessionShellInput = {
|
||||
readonly sessionID: { readonly sessionID: string }["sessionID"]
|
||||
@@ -3862,10 +3880,14 @@ export type SessionShellOutput = void
|
||||
|
||||
export type SessionCompactInput = {
|
||||
readonly sessionID: { readonly sessionID: string }["sessionID"]
|
||||
readonly id?: { readonly id?: string | undefined }["id"]
|
||||
readonly id?: { readonly id?: string | undefined; readonly delivery?: ("steer" | "queue") | undefined }["id"]
|
||||
readonly delivery?: {
|
||||
readonly id?: string | undefined
|
||||
readonly delivery?: ("steer" | "queue") | undefined
|
||||
}["delivery"]
|
||||
}
|
||||
|
||||
export type SessionCompactOutput = { data: SessionPendingCompaction }["data"]
|
||||
export type SessionCompactOutput = { data: SessionInboxCompaction }["data"]
|
||||
|
||||
export type SessionWaitInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] }
|
||||
|
||||
@@ -3891,30 +3913,30 @@ export type SessionContextInput = { readonly sessionID: { readonly sessionID: st
|
||||
|
||||
export type SessionContextOutput = { data: Array<SessionMessageInfo> }["data"]
|
||||
|
||||
export type SessionPendingListInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] }
|
||||
export type SessionInboxListInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] }
|
||||
|
||||
export type SessionPendingListOutput = { data: Array<SessionPendingInfo> }["data"]
|
||||
export type SessionInboxListOutput = { data: Array<SessionInboxInfo> }["data"]
|
||||
|
||||
export type SessionPendingCancelInput = {
|
||||
readonly sessionID: { readonly sessionID: string; readonly inputID: string }["sessionID"]
|
||||
readonly inputID: { readonly sessionID: string; readonly inputID: string }["inputID"]
|
||||
export type SessionInboxCancelInput = {
|
||||
readonly sessionID: { readonly sessionID: string; readonly inboxID: string }["sessionID"]
|
||||
readonly inboxID: { readonly sessionID: string; readonly inboxID: string }["inboxID"]
|
||||
}
|
||||
|
||||
export type SessionPendingCancelOutput = void
|
||||
export type SessionInboxCancelOutput = void
|
||||
|
||||
export type SessionPendingSteerInput = {
|
||||
readonly sessionID: { readonly sessionID: string; readonly inputID: string }["sessionID"]
|
||||
readonly inputID: { readonly sessionID: string; readonly inputID: string }["inputID"]
|
||||
export type SessionInboxSteerInput = {
|
||||
readonly sessionID: { readonly sessionID: string; readonly inboxID: string }["sessionID"]
|
||||
readonly inboxID: { readonly sessionID: string; readonly inboxID: string }["inboxID"]
|
||||
}
|
||||
|
||||
export type SessionPendingSteerOutput = void
|
||||
export type SessionInboxSteerOutput = void
|
||||
|
||||
export type SessionPendingQueueInput = {
|
||||
readonly sessionID: { readonly sessionID: string; readonly inputID: string }["sessionID"]
|
||||
readonly inputID: { readonly sessionID: string; readonly inputID: string }["inputID"]
|
||||
export type SessionInboxQueueInput = {
|
||||
readonly sessionID: { readonly sessionID: string; readonly inboxID: string }["sessionID"]
|
||||
readonly inboxID: { readonly sessionID: string; readonly inboxID: string }["inboxID"]
|
||||
}
|
||||
|
||||
export type SessionPendingQueueOutput = void
|
||||
export type SessionInboxQueueOutput = void
|
||||
|
||||
export type SessionInstructionsEntryListInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] }
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
spawnServiceContender,
|
||||
} from "../service-contender.js"
|
||||
import { defaultEnsureTiming, ensureTiming, type EnsureTiming } from "../service-timing.js"
|
||||
import { matchesVersion } from "../service-version.js"
|
||||
import type { ServiceHealth, ServiceStopResponse } from "./generated/types.js"
|
||||
|
||||
export * from "../service.js"
|
||||
@@ -27,7 +28,7 @@ export async function discover(options: DiscoverOptions = {}) {
|
||||
async function discoverLocal(options: DiscoverOptions) {
|
||||
const found = (await registered(options.file)).service
|
||||
if (found?.state !== "ready") return undefined
|
||||
if (options.version !== undefined && found.version !== options.version) return undefined
|
||||
if (!matchesVersion(found.version, options)) return undefined
|
||||
return found
|
||||
}
|
||||
|
||||
@@ -76,7 +77,7 @@ export async function ensure(options: EnsureOptions = {}): Promise<Endpoint> {
|
||||
if (registration.service !== undefined) {
|
||||
spawnDelay = timing.spawnDelay
|
||||
const service = registration.service
|
||||
const compatible = !service.legacy && (options.version === undefined || service.version === options.version)
|
||||
const compatible = !service.legacy && matchesVersion(service.version, options)
|
||||
if (compatible && service.state === "ready") return service.endpoint
|
||||
if (compatible && service.state === "failed") throw new Error("Background service failed to start")
|
||||
if (!compatible) {
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { DiscoverOptions } from "./service.js"
|
||||
|
||||
export function matchesVersion(version: string | undefined, options: DiscoverOptions) {
|
||||
if (options.version === undefined) return true
|
||||
if (version === undefined) return false
|
||||
if (typeof options.version === "function") return options.version(version)
|
||||
return version === options.version
|
||||
}
|
||||
@@ -17,8 +17,8 @@ export type Endpoint = {
|
||||
export type DiscoverOptions = {
|
||||
/** Absolute registration file path. Defaults to the XDG state directory. */
|
||||
readonly file?: string
|
||||
/** Required service version. */
|
||||
readonly version?: string
|
||||
/** Required exact service version or compatibility predicate. */
|
||||
readonly version?: string | ((version: string) => boolean)
|
||||
}
|
||||
|
||||
/** Reason ensuring the service requires a new process. */
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import { OpenCode as EffectOpenCode, type AppApi as EffectApi } from "../src/effect"
|
||||
import type { Session } from "@opencode-ai/schema/session"
|
||||
import type { DiscoverOptions } from "../src/service"
|
||||
|
||||
type EffectClient = Effect.Success<ReturnType<typeof EffectOpenCode.make>>
|
||||
type PromiseClient = ReturnType<typeof import("../src/promise").OpenCode.make>
|
||||
@@ -8,6 +9,9 @@ type PromiseClient = ReturnType<typeof import("../src/promise").OpenCode.make>
|
||||
declare const effectClient: EffectClient
|
||||
declare const promiseClient: PromiseClient
|
||||
|
||||
const exactVersion: DiscoverOptions = { version: "2.0.0" }
|
||||
const compatibleVersion: DiscoverOptions = { version: (version) => version.startsWith("2.") }
|
||||
|
||||
const effectApi: EffectApi<unknown> = effectClient
|
||||
|
||||
const effectSession: Effect.Effect<Session.Info, unknown> = effectClient.session.get({
|
||||
@@ -42,4 +46,14 @@ const promiseRemove: Promise<void> = promiseClient.session.instructions.entry.re
|
||||
key: "review-notes",
|
||||
})
|
||||
|
||||
void [effectSession, effectList, effectPut, effectRemove, promiseList, promisePut, promiseRemove]
|
||||
void [
|
||||
effectSession,
|
||||
effectList,
|
||||
effectPut,
|
||||
effectRemove,
|
||||
promiseList,
|
||||
promisePut,
|
||||
promiseRemove,
|
||||
exactVersion,
|
||||
compatibleVersion,
|
||||
]
|
||||
|
||||
@@ -213,7 +213,7 @@ test("session methods retain decoded Effect inputs and outputs", async () => {
|
||||
expect(Object.getPrototypeOf(result.created)).toBe(Object.prototype)
|
||||
expect(result.created.id).toBe("ses_test")
|
||||
expect(Object.getPrototypeOf(result.admitted)).toBe(Object.prototype)
|
||||
expect(Object.getPrototypeOf(result.admitted.data)).toBe(Object.prototype)
|
||||
expect(Object.getPrototypeOf(result.admitted.payload)).toBe(Object.prototype)
|
||||
expect(DateTime.toEpochMillis(result.admitted.timeCreated)).toBe(1_717_171_717_000)
|
||||
expect(result.context).toEqual([])
|
||||
expect(logQueries[0]).toEqual({ after: "0" })
|
||||
@@ -271,7 +271,7 @@ const admission = {
|
||||
id: "msg_test",
|
||||
sessionID: "ses_test",
|
||||
type: "user",
|
||||
data: { text: "Hello" },
|
||||
payload: { text: "Hello" },
|
||||
delivery: "steer",
|
||||
timeCreated: 1_717_171_717_000,
|
||||
},
|
||||
@@ -280,6 +280,8 @@ const admission = {
|
||||
const compactionAdmission = {
|
||||
data: {
|
||||
type: "compaction",
|
||||
payload: {},
|
||||
delivery: "queue",
|
||||
id: "msg_compaction",
|
||||
sessionID: "ses_test",
|
||||
timeCreated: 1_717_171_717_000,
|
||||
|
||||
@@ -27,7 +27,10 @@ if (mode === "delayed" || mode === "delayed-failed" || mode === "coordinated" ||
|
||||
}
|
||||
|
||||
let requests = 0
|
||||
const version = mode === "old" || mode === "reject-stop" ? "old" : "test"
|
||||
let version = "test"
|
||||
if (mode === "old" || mode === "reject-stop") version = "old"
|
||||
if (mode === "incompatible") version = "1.9.0"
|
||||
if (mode === "compatible" || mode === "delayed-compatible") version = "2.1.0-next.1"
|
||||
const id = crypto.randomUUID()
|
||||
const server = Bun.serve({
|
||||
port: 0,
|
||||
|
||||
@@ -25,6 +25,19 @@ test("discovers a registered service", async () => {
|
||||
expect(await Service.discover({ file: registration, version: "other" })).toBeUndefined()
|
||||
})
|
||||
|
||||
test("discovers a compatible registered service", async () => {
|
||||
const registration = await setup("compatible")
|
||||
|
||||
expect(await Service.discover({ file: registration, version: "2.1.0" })).toBeUndefined()
|
||||
expect(await Service.discover({ file: registration, version: "2.1.0-next.1" })).toEqual(
|
||||
expect.objectContaining({ url: expect.stringMatching(/^http:\/\//) }),
|
||||
)
|
||||
expect(await Service.discover({ file: registration, version: (version) => version.startsWith("2.") })).toEqual(
|
||||
expect.objectContaining({ url: expect.stringMatching(/^http:\/\//) }),
|
||||
)
|
||||
expect(await Service.discover({ file: registration, version: (version) => version.startsWith("3.") })).toBeUndefined()
|
||||
})
|
||||
|
||||
test("ensures a missing service with native promises", async () => {
|
||||
const directory = await temp()
|
||||
const registration = join(directory, "service.json")
|
||||
|
||||
@@ -373,7 +373,7 @@ test("session instructions methods use the public HTTP contract", async () => {
|
||||
])
|
||||
})
|
||||
|
||||
test("session.pending.list uses the public HTTP contract", async () => {
|
||||
test("session.inbox.list uses the public HTTP contract", async () => {
|
||||
const requests: Array<{ method: string; url: string }> = []
|
||||
const pending = [
|
||||
{
|
||||
@@ -381,7 +381,7 @@ test("session.pending.list uses the public HTTP contract", async () => {
|
||||
sessionID: "ses_test",
|
||||
timeCreated: 1_717_171_717_000,
|
||||
type: "user",
|
||||
data: { text: "Fix the failing tests" },
|
||||
payload: { text: "Fix the failing tests" },
|
||||
delivery: "steer",
|
||||
},
|
||||
]
|
||||
@@ -394,13 +394,13 @@ test("session.pending.list uses the public HTTP contract", async () => {
|
||||
},
|
||||
})
|
||||
|
||||
const result = await client.session.pending.list({ sessionID: "ses_test" })
|
||||
const result = await client.session.inbox.list({ sessionID: "ses_test" })
|
||||
|
||||
expect(result).toEqual(pending)
|
||||
expect(requests).toEqual([{ method: "GET", url: "http://localhost:3000/api/session/ses_test/pending" }])
|
||||
expect(requests).toEqual([{ method: "GET", url: "http://localhost:3000/api/session/ses_test/inbox" }])
|
||||
})
|
||||
|
||||
test("session.pending mutations use the public HTTP contract", async () => {
|
||||
test("session.inbox mutations use the public HTTP contract", async () => {
|
||||
const requests: Array<{ method: string; url: string }> = []
|
||||
const client = OpenCode.make({
|
||||
baseUrl: "http://localhost:3000",
|
||||
@@ -411,14 +411,14 @@ test("session.pending mutations use the public HTTP contract", async () => {
|
||||
},
|
||||
})
|
||||
|
||||
await client.session.pending.cancel({ sessionID: "ses_test", inputID: "msg_cancel" })
|
||||
await client.session.pending.steer({ sessionID: "ses_test", inputID: "msg_steer" })
|
||||
await client.session.pending.queue({ sessionID: "ses_test", inputID: "msg_queue" })
|
||||
await client.session.inbox.cancel({ sessionID: "ses_test", inboxID: "msg_cancel" })
|
||||
await client.session.inbox.steer({ sessionID: "ses_test", inboxID: "msg_steer" })
|
||||
await client.session.inbox.queue({ sessionID: "ses_test", inboxID: "msg_queue" })
|
||||
|
||||
expect(requests).toEqual([
|
||||
{ method: "DELETE", url: "http://localhost:3000/api/session/ses_test/pending/msg_cancel" },
|
||||
{ method: "POST", url: "http://localhost:3000/api/session/ses_test/pending/msg_steer/steer" },
|
||||
{ method: "POST", url: "http://localhost:3000/api/session/ses_test/pending/msg_queue/queue" },
|
||||
{ method: "DELETE", url: "http://localhost:3000/api/session/ses_test/inbox/msg_cancel" },
|
||||
{ method: "POST", url: "http://localhost:3000/api/session/ses_test/inbox/msg_steer/steer" },
|
||||
{ method: "POST", url: "http://localhost:3000/api/session/ses_test/inbox/msg_queue/queue" },
|
||||
])
|
||||
})
|
||||
|
||||
|
||||
@@ -47,6 +47,52 @@ test("a concurrent same-version start cannot invalidate a resolved endpoint", as
|
||||
expect(await health(resolved.url)).toEqual({ healthy: true, version: "test", pid: original.pid })
|
||||
})
|
||||
|
||||
test("reuses a compatible registered service", async () => {
|
||||
const directory = await temp()
|
||||
const registration = join(directory, "service.json")
|
||||
const existing = spawn(registration, "compatible")
|
||||
await waitForFile(registration)
|
||||
|
||||
const starts: EnsureReason[] = []
|
||||
const endpoint = await run(
|
||||
ensure({
|
||||
file: registration,
|
||||
version: (version) => version.startsWith("2."),
|
||||
command: [],
|
||||
onStart: (reason) => starts.push(reason),
|
||||
}),
|
||||
)
|
||||
|
||||
expect(endpoint.url).toBe((await Bun.file(registration).json()).url)
|
||||
expect(starts).toEqual([])
|
||||
expect(existing.exitCode).toBe(null)
|
||||
})
|
||||
|
||||
test("replaces an incompatible registered service", async () => {
|
||||
const directory = await temp()
|
||||
const registration = join(directory, "service.json")
|
||||
const existing = spawn(registration, "incompatible")
|
||||
await waitForFile(registration)
|
||||
|
||||
const starts: EnsureReason[] = []
|
||||
const endpoint = await run(
|
||||
ensure({
|
||||
file: registration,
|
||||
version: (version) => version.startsWith("2."),
|
||||
command: [process.execPath, fixture, registration, "delayed-compatible", "10"],
|
||||
onStart: (reason) => starts.push(reason),
|
||||
}),
|
||||
)
|
||||
const replacement = await Bun.file(registration).json()
|
||||
|
||||
expect(await existing.exited).toBe(0)
|
||||
expect(replacement.version).toBe("2.1.0-next.1")
|
||||
expect(endpoint.url).toBe(replacement.url)
|
||||
expect(starts).toEqual(["version-mismatch"])
|
||||
process.kill(replacement.pid, "SIGTERM")
|
||||
await waitForExit(replacement.pid)
|
||||
})
|
||||
|
||||
test("waits for a registered service to finish starting", async () => {
|
||||
const directory = await temp()
|
||||
const registration = join(directory, "service.json")
|
||||
|
||||
@@ -19,6 +19,12 @@
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"imports": {
|
||||
"#transpile": {
|
||||
"workerd": "./src/interpreter/transpile.workerd.ts",
|
||||
"default": "./src/interpreter/transpile.node.ts"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "bun run script/build.ts",
|
||||
"typecheck": "tsgo --noEmit",
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { parse } from "acorn"
|
||||
import { Cause, Effect, Scope } from "effect"
|
||||
import { DiagnosticCategory, ModuleKind, ScriptTarget, flattenDiagnosticMessageText, transpileModule } from "typescript"
|
||||
// #transpile: conditional import — full typescript on node/bun, an identity
|
||||
// pass-through on workerd (the compiler is ~11 MiB and can't init there).
|
||||
import { transpile } from "#transpile"
|
||||
import type { DataValue, Diagnostic, ExecuteOptions, ResolvedExecutionLimits, Result } from "../codemode.js"
|
||||
import { copyIn, copyOut, ToolRuntime, type Services } from "../tool-runtime.js"
|
||||
import type { Tools } from "../tools.js"
|
||||
@@ -119,21 +121,10 @@ export const executeWithLimits = <const Provided extends Record<string, unknown>
|
||||
}
|
||||
|
||||
const parseProgram = (code: string): ProgramNode => {
|
||||
const transpiled = transpileModule(`async function __codemode__() {\n${code}\n}`, {
|
||||
reportDiagnostics: true,
|
||||
compilerOptions: {
|
||||
target: ScriptTarget.ESNext,
|
||||
module: ModuleKind.ESNext,
|
||||
},
|
||||
})
|
||||
const diagnostic = transpiled.diagnostics?.find((item) => item.category === DiagnosticCategory.Error)
|
||||
const transpiled = transpile(`async function __codemode__() {\n${code}\n}`)
|
||||
|
||||
if (diagnostic) {
|
||||
throw new InterpreterRuntimeError(
|
||||
`Failed to parse TypeScript: ${flattenDiagnosticMessageText(diagnostic.messageText, "\n")}`,
|
||||
undefined,
|
||||
"ParseError",
|
||||
)
|
||||
if (transpiled.error !== undefined) {
|
||||
throw new InterpreterRuntimeError(`Failed to parse TypeScript: ${transpiled.error}`, undefined, "ParseError")
|
||||
}
|
||||
|
||||
const bodyStart = transpiled.outputText.indexOf("{") + 1
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { DiagnosticCategory, ModuleKind, ScriptTarget, flattenDiagnosticMessageText, transpileModule } from "typescript"
|
||||
|
||||
export interface TranspileResult {
|
||||
readonly outputText: string
|
||||
readonly error?: string
|
||||
}
|
||||
|
||||
// Full TypeScript transpilation on node/bun runtimes.
|
||||
export const transpile = (source: string): TranspileResult => {
|
||||
const transpiled = transpileModule(source, {
|
||||
reportDiagnostics: true,
|
||||
compilerOptions: {
|
||||
target: ScriptTarget.ESNext,
|
||||
module: ModuleKind.ESNext,
|
||||
},
|
||||
})
|
||||
const diagnostic = transpiled.diagnostics?.find((item) => item.category === DiagnosticCategory.Error)
|
||||
if (diagnostic) {
|
||||
return {
|
||||
outputText: transpiled.outputText,
|
||||
error: flattenDiagnosticMessageText(diagnostic.messageText, "\n"),
|
||||
}
|
||||
}
|
||||
return { outputText: transpiled.outputText }
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export interface TranspileResult {
|
||||
readonly outputText: string
|
||||
readonly error?: string
|
||||
}
|
||||
|
||||
// workerd profile: the typescript compiler is ~11 MiB and probes node
|
||||
// internals at module init, so codemode programs are passed through
|
||||
// untranspiled. Plain-JS programs (the overwhelmingly common case) parse
|
||||
// fine downstream via acorn; TypeScript-only syntax surfaces as a parse
|
||||
// error from the interpreter instead of a transpile diagnostic.
|
||||
export const transpile = (source: string): TranspileResult => ({ outputText: source })
|
||||
@@ -36,26 +36,31 @@
|
||||
"default": "./src/database/sqlite.node.ts"
|
||||
},
|
||||
"#pty": {
|
||||
"workerd": "./src/pty/pty.workerd.ts",
|
||||
"bun": "./src/pty/pty.bun.ts",
|
||||
"node": "./src/pty/pty.node.ts",
|
||||
"default": "./src/pty/pty.bun.ts"
|
||||
},
|
||||
"#fff": {
|
||||
"workerd": "./src/filesystem/fff.workerd.ts",
|
||||
"bun": "./src/filesystem/fff.bun.ts",
|
||||
"node": "./src/filesystem/fff.node.ts",
|
||||
"default": "./src/filesystem/fff.bun.ts"
|
||||
},
|
||||
"#photon-wasm": {
|
||||
"workerd": "./src/image/photon-wasm.workerd.ts",
|
||||
"bun": "./src/image/photon-wasm.bun.ts",
|
||||
"node": "./src/image/photon-wasm.node.ts",
|
||||
"default": "./src/image/photon-wasm.bun.ts"
|
||||
},
|
||||
"#shell-parser-wasm": {
|
||||
"workerd": "./src/shell/parser-wasm.workerd.ts",
|
||||
"bun": "./src/shell/parser-wasm.bun.ts",
|
||||
"node": "./src/shell/parser-wasm.node.ts",
|
||||
"default": "./src/shell/parser-wasm.bun.ts"
|
||||
},
|
||||
"#process-lock-ffi": {
|
||||
"workerd": "./src/util/process-lock-ffi.workerd.ts",
|
||||
"bun": "./src/util/process-lock-ffi.bun.ts",
|
||||
"node": "./src/util/process-lock-ffi.node.ts",
|
||||
"default": "./src/util/process-lock-ffi.bun.ts"
|
||||
|
||||
+134
-2
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "sqlite",
|
||||
"id": "00924d88-1842-4d71-ac74-5682ddc47e1c",
|
||||
"prevIds": ["15060ec5-05f7-4b86-b2a5-9108609432b3"],
|
||||
"id": "5c1aa56b-c3ee-4283-9a84-c0bf626dc604",
|
||||
"prevIds": ["00924d88-1842-4d71-ac74-5682ddc47e1c"],
|
||||
"ddl": [
|
||||
{
|
||||
"name": "account_state",
|
||||
@@ -56,6 +56,10 @@
|
||||
"name": "instruction_state",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "session_inbox",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "session_message",
|
||||
"entityType": "tables"
|
||||
@@ -842,6 +846,76 @@
|
||||
"entityType": "columns",
|
||||
"table": "instruction_state"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "id",
|
||||
"entityType": "columns",
|
||||
"table": "session_inbox"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "session_id",
|
||||
"entityType": "columns",
|
||||
"table": "session_inbox"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "type",
|
||||
"entityType": "columns",
|
||||
"table": "session_inbox"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "payload",
|
||||
"entityType": "columns",
|
||||
"table": "session_inbox"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "delivery",
|
||||
"entityType": "columns",
|
||||
"table": "session_inbox"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "enqueued_seq",
|
||||
"entityType": "columns",
|
||||
"table": "session_inbox"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "session_inbox"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
@@ -1428,6 +1502,17 @@
|
||||
"entityType": "fks",
|
||||
"table": "instruction_state"
|
||||
},
|
||||
{
|
||||
"columns": ["session_id"],
|
||||
"tableTo": "session_v2",
|
||||
"columnsTo": ["id"],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_session_inbox_session_id_session_v2_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "session_inbox"
|
||||
},
|
||||
{
|
||||
"columns": ["session_id"],
|
||||
"tableTo": "session_v2",
|
||||
@@ -1552,6 +1637,13 @@
|
||||
"table": "instruction_state",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["id"],
|
||||
"nameExplicit": false,
|
||||
"name": "session_inbox_pk",
|
||||
"table": "session_inbox",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["id"],
|
||||
"nameExplicit": false,
|
||||
@@ -1642,6 +1734,46 @@
|
||||
"entityType": "indexes",
|
||||
"table": "permission"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "session_id",
|
||||
"isExpression": false
|
||||
},
|
||||
{
|
||||
"value": "delivery",
|
||||
"isExpression": false
|
||||
},
|
||||
{
|
||||
"value": "enqueued_seq",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "session_inbox_session_delivery_seq_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "session_inbox"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "session_id",
|
||||
"isExpression": false
|
||||
},
|
||||
{
|
||||
"value": "enqueued_seq",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "session_inbox_session_enqueued_seq_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "session_inbox"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
|
||||
@@ -122,7 +122,10 @@ const layer = Layer.effect(
|
||||
return { id: info?.id ?? defaultID, info }
|
||||
}),
|
||||
list: Effect.fn("Agent.list")(function* () {
|
||||
return Array.fromIterable(state.get().agents.values())
|
||||
const agents = Array.fromIterable(state.get().agents.values())
|
||||
const selected = selectedDefault()
|
||||
if (!selected) return agents
|
||||
return [selected, ...agents.filter((agent) => agent.id !== selected.id)]
|
||||
}),
|
||||
})
|
||||
}),
|
||||
|
||||
+200
-31
@@ -6,7 +6,8 @@ import type { EventLog } from "@opencode-ai/schema/event-log"
|
||||
import { and, asc, eq, gt, lte, sql } from "drizzle-orm"
|
||||
import { Database } from "./database/database.js"
|
||||
import { EventSequenceTable, EventTable } from "./event/sql.js"
|
||||
import { Location } from "./location.js"
|
||||
import type { Location } from "@opencode-ai/schema/location"
|
||||
import { KeyedMutex } from "./effect/keyed-mutex.js"
|
||||
import { makeGlobalNode } from "@opencode-ai/util/effect/app-node"
|
||||
import { isDeepStrictEqual } from "node:util"
|
||||
import { Durable } from "@opencode-ai/schema/durable-event-manifest"
|
||||
@@ -92,6 +93,16 @@ export interface PublishOptions {
|
||||
readonly commit?: (seq: number) => Effect.Effect<void>
|
||||
}
|
||||
|
||||
export type PublishInput<D extends Event.DurableDefinition = Event.DurableDefinition> = readonly [
|
||||
definition: D,
|
||||
data: Event.Data<D>,
|
||||
options?: PublishOptions,
|
||||
]
|
||||
|
||||
export type PublishResult<I extends readonly PublishInput[]> = {
|
||||
readonly [K in keyof I]: I[K] extends PublishInput<infer D> ? Event.Payload<D> : never
|
||||
}
|
||||
|
||||
/** Marker/event union emitted by `log`. */
|
||||
export type LogItem = Event.Payload | EventLog.Synced
|
||||
|
||||
@@ -124,6 +135,9 @@ export interface Interface {
|
||||
data: Event.Data<D>,
|
||||
options?: PublishOptions,
|
||||
) => Effect.Effect<Event.Payload<D>>
|
||||
readonly publishAll: <const I extends readonly [PublishInput, ...PublishInput[]]>(
|
||||
events: I,
|
||||
) => Effect.Effect<PublishResult<I>>
|
||||
readonly subscribe: Subscribe
|
||||
/**
|
||||
* Durable, ordered per-aggregate log read. Forked aggregates may reserve an
|
||||
@@ -169,6 +183,9 @@ export function configured(options?: Options) {
|
||||
layer: Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
// Deferred import: a static one would close the module cycle
|
||||
// bus → location → project → bus and hit the node bindings in TDZ.
|
||||
const { Location } = yield* Effect.promise(() => import("./location.js"))
|
||||
const pubsub = {
|
||||
live: yield* PubSub.unbounded<Event.Payload>(),
|
||||
durable: new Map<string, Set<PubSub.PubSub<void>>>(),
|
||||
@@ -176,6 +193,7 @@ export function configured(options?: Options) {
|
||||
}
|
||||
const projectors = new Map<string, Subscriber[]>()
|
||||
const listeners = new Array<Subscriber>()
|
||||
const durableLocks = KeyedMutex.makeUnsafe<string>()
|
||||
const { db } = yield* Database.Service
|
||||
const logReadPageSize = options?.logReadPageSize ?? 512
|
||||
const persist = options?.persist ?? false
|
||||
@@ -385,15 +403,23 @@ export function configured(options?: Options) {
|
||||
}),
|
||||
)
|
||||
if (definition?.durable) {
|
||||
const committed = yield* commitDurableEvent(definition, event as Event.Payload, undefined, commit)
|
||||
if (committed) {
|
||||
event = {
|
||||
...event,
|
||||
durable: envelope(committed.aggregateID, committed.seq, definition.durable.version),
|
||||
}
|
||||
yield* notify(event as Event.Payload, true)
|
||||
return event
|
||||
}
|
||||
const aggregateID = (event.data as Record<string, unknown>)[definition.durable.aggregate]
|
||||
if (typeof aggregateID !== "string")
|
||||
return yield* commitDurableEvent(definition, event as Event.Payload, undefined, commit).pipe(
|
||||
Effect.as(event),
|
||||
)
|
||||
return yield* durableLocks.withLock(aggregateID)(
|
||||
Effect.gen(function* () {
|
||||
const committed = yield* commitDurableEvent(definition, event as Event.Payload, undefined, commit)
|
||||
if (!committed) return event
|
||||
event = {
|
||||
...event,
|
||||
durable: envelope(committed.aggregateID, committed.seq, definition.durable.version),
|
||||
}
|
||||
yield* notify(event as Event.Payload, true)
|
||||
return event
|
||||
}),
|
||||
)
|
||||
}
|
||||
yield* notify(event as Event.Payload, false)
|
||||
return event
|
||||
@@ -444,6 +470,144 @@ export function configured(options?: Options) {
|
||||
})
|
||||
}
|
||||
|
||||
function publishAll<const I extends readonly [PublishInput, ...PublishInput[]]>(events: I) {
|
||||
return Effect.gen(function* () {
|
||||
const serviceLocation = Option.getOrUndefined(yield* Effect.serviceOption(Location.Service))
|
||||
const payloads = yield* Effect.forEach(events, ([definition, data, options]) =>
|
||||
Effect.gen(function* () {
|
||||
const aggregateID = (data as Record<string, unknown>)[definition.durable.aggregate]
|
||||
if (typeof aggregateID !== "string") {
|
||||
return yield* Effect.die(
|
||||
new InvalidDurableEventError({
|
||||
type: definition.type,
|
||||
message: `Expected string aggregate field ${definition.durable.aggregate}`,
|
||||
}),
|
||||
)
|
||||
}
|
||||
const location =
|
||||
options?.location ??
|
||||
(serviceLocation
|
||||
? { directory: serviceLocation.directory, workspaceID: serviceLocation.workspaceID }
|
||||
: undefined)
|
||||
return {
|
||||
definition,
|
||||
aggregateID,
|
||||
commit: options?.commit,
|
||||
event: {
|
||||
id: options?.id ?? Event.ID.create(),
|
||||
created: yield* DateTime.now,
|
||||
...(options?.metadata ? { metadata: options.metadata } : {}),
|
||||
type: definition.type,
|
||||
...(location ? { location } : {}),
|
||||
data,
|
||||
} as Event.Payload,
|
||||
}
|
||||
}),
|
||||
)
|
||||
const aggregateID = payloads[0].aggregateID
|
||||
if (payloads.some((item) => item.aggregateID !== aggregateID)) {
|
||||
return yield* Effect.die(
|
||||
new InvalidDurableEventError({
|
||||
type: payloads[0].definition.type,
|
||||
message: "Published events must belong to the same aggregate",
|
||||
}),
|
||||
)
|
||||
}
|
||||
return yield* durableLocks.withLock(aggregateID)(
|
||||
Effect.uninterruptible(
|
||||
Effect.gen(function* () {
|
||||
const committed = yield* db
|
||||
.transaction(
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const row = yield* db
|
||||
.select({ seq: EventSequenceTable.seq })
|
||||
.from(EventSequenceTable)
|
||||
.where(eq(EventSequenceTable.aggregate_id, aggregateID))
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
const firstSeq = (row?.seq ?? -1) + 1
|
||||
const finalSeq = firstSeq + payloads.length - 1
|
||||
const result = new Array<Event.Payload>()
|
||||
const rows = new Array<typeof EventTable.$inferInsert>()
|
||||
const ids = new Set<Event.ID>()
|
||||
for (const [index, item] of payloads.entries()) {
|
||||
const seq = firstSeq + index
|
||||
const encoded = Schema.encodeUnknownSync(item.definition.data)(item.event.data) as Record<
|
||||
string,
|
||||
unknown
|
||||
>
|
||||
if (persist) {
|
||||
if (ids.has(item.event.id))
|
||||
yield* Effect.die(
|
||||
new InvalidDurableEventError({
|
||||
type: item.event.type,
|
||||
message: `Event ${item.event.id} appears more than once in the batch`,
|
||||
}),
|
||||
)
|
||||
ids.add(item.event.id)
|
||||
const stored = yield* db
|
||||
.select({ aggregateID: EventTable.aggregate_id, seq: EventTable.seq })
|
||||
.from(EventTable)
|
||||
.where(eq(EventTable.id, item.event.id))
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
if (stored)
|
||||
yield* Effect.die(
|
||||
new InvalidDurableEventError({
|
||||
type: item.event.type,
|
||||
message: `Event ${item.event.id} already exists at aggregate ${stored.aggregateID} sequence ${stored.seq}`,
|
||||
}),
|
||||
)
|
||||
}
|
||||
const event = {
|
||||
...item.event,
|
||||
durable: envelope(aggregateID, seq, item.definition.durable.version),
|
||||
} as Event.Payload
|
||||
for (const projector of projectors.get(
|
||||
versionedType(item.definition.type, item.definition.durable.version),
|
||||
) ?? []) {
|
||||
yield* projector(event)
|
||||
}
|
||||
if (item.commit) yield* item.commit(seq)
|
||||
if (persist)
|
||||
rows.push({
|
||||
id: event.id,
|
||||
aggregate_id: aggregateID,
|
||||
seq,
|
||||
created: DateTime.toEpochMillis(event.created),
|
||||
type: versionedType(item.definition.type, item.definition.durable.version),
|
||||
data: encoded,
|
||||
})
|
||||
result.push(event)
|
||||
}
|
||||
yield* db
|
||||
.insert(EventSequenceTable)
|
||||
.values([{ aggregate_id: aggregateID, seq: finalSeq }])
|
||||
.onConflictDoUpdate({ target: EventSequenceTable.aggregate_id, set: { seq: finalSeq } })
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
if (persist) yield* db.insert(EventTable).values(rows).run().pipe(Effect.orDie)
|
||||
return result
|
||||
}),
|
||||
{ behavior: "immediate" },
|
||||
)
|
||||
.pipe(Effect.orDie)
|
||||
yield* Effect.forEach(
|
||||
pubsub.durable.get(aggregateID) ?? [],
|
||||
(wake) => PubSub.publish(wake, undefined),
|
||||
{
|
||||
discard: true,
|
||||
},
|
||||
)
|
||||
yield* Effect.forEach(committed, (event) => notify(event, true), { discard: true })
|
||||
return committed as PublishResult<I>
|
||||
}),
|
||||
),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
function replay(
|
||||
event: SerializedEvent,
|
||||
options?: { readonly publish?: boolean; readonly ownerID?: string; readonly strictOwner?: boolean },
|
||||
@@ -455,27 +619,31 @@ export function configured(options?: Options) {
|
||||
new InvalidDurableEventError({ type: event.type, message: `Unknown durable event type ${event.type}` }),
|
||||
)
|
||||
} else {
|
||||
const payload = {
|
||||
id: event.id,
|
||||
created: event.created ?? DateTime.makeUnsafe(0),
|
||||
type: definition.type,
|
||||
data: Schema.decodeUnknownSync(definition.data)(event.data),
|
||||
} as Event.Payload
|
||||
const committed = yield* commitDurableEvent(definition, payload, {
|
||||
seq: event.seq,
|
||||
aggregateID: event.aggregateID,
|
||||
ownerID: options?.ownerID,
|
||||
strictOwner: options?.strictOwner,
|
||||
})
|
||||
if (committed && options?.publish) {
|
||||
yield* notify(
|
||||
{
|
||||
...payload,
|
||||
durable: envelope(committed.aggregateID, committed.seq, definition.durable.version),
|
||||
},
|
||||
true,
|
||||
)
|
||||
}
|
||||
yield* durableLocks.withLock(event.aggregateID)(
|
||||
Effect.gen(function* () {
|
||||
const payload = {
|
||||
id: event.id,
|
||||
created: event.created ?? DateTime.makeUnsafe(0),
|
||||
type: definition.type,
|
||||
data: Schema.decodeUnknownSync(definition.data)(event.data),
|
||||
} as Event.Payload
|
||||
const committed = yield* commitDurableEvent(definition, payload, {
|
||||
seq: event.seq,
|
||||
aggregateID: event.aggregateID,
|
||||
ownerID: options?.ownerID,
|
||||
strictOwner: options?.strictOwner,
|
||||
})
|
||||
if (committed && options?.publish) {
|
||||
yield* notify(
|
||||
{
|
||||
...payload,
|
||||
durable: envelope(committed.aggregateID, committed.seq, definition.durable.version),
|
||||
},
|
||||
true,
|
||||
)
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -692,6 +860,7 @@ export function configured(options?: Options) {
|
||||
|
||||
return Service.of({
|
||||
publish,
|
||||
publishAll,
|
||||
subscribe,
|
||||
log,
|
||||
listen,
|
||||
|
||||
@@ -65,4 +65,13 @@ export function configured(options?: Options) {
|
||||
return makeGlobalNode({ service: Service, layer: layer(options), deps: [Global.node] })
|
||||
}
|
||||
|
||||
/** `configured`, but over an injected SqlClient layer instead of a filesystem path. */
|
||||
export function configuredClient(client: Layer.Layer<SqlClient.SqlClient>) {
|
||||
return makeGlobalNode({
|
||||
service: Service,
|
||||
layer: layerFromClient.pipe(Layer.provide(client)),
|
||||
deps: [Global.node],
|
||||
})
|
||||
}
|
||||
|
||||
export const node = configured({ path: ":memory:" })
|
||||
|
||||
@@ -28,6 +28,20 @@ export interface EffectSQLiteQueryEffectHKT extends QueryEffectHKTBase {
|
||||
readonly context: never
|
||||
}
|
||||
|
||||
/**
|
||||
* A SqlClient whose runtime rejects SQL transaction statements (BEGIN/COMMIT/SAVEPOINT) and
|
||||
* manages transactions natively instead — Cloudflare Durable Object SQLite. Producers set
|
||||
* `transactionStatements: false` (see `database/sqlite.workerd.ts`) and the session delegates
|
||||
* to the client's `withTransaction` rather than issuing transaction statements.
|
||||
*/
|
||||
export interface NativeTransactionSqlClient extends SqlClient {
|
||||
readonly transactionStatements: false
|
||||
}
|
||||
|
||||
export function managesTransactionsNatively(client: SqlClient): client is NativeTransactionSqlClient {
|
||||
return (client as SqlClient & { readonly transactionStatements?: boolean }).transactionStatements === false
|
||||
}
|
||||
|
||||
export type EffectSQLiteRunResult = readonly never[]
|
||||
|
||||
export interface EffectSQLiteSessionOptions {
|
||||
@@ -120,6 +134,7 @@ export class EffectSQLiteSession<TRelations extends AnyRelations> extends SQLite
|
||||
}
|
||||
|
||||
private withTransaction<A, E, R>(effect: Effect.Effect<A, E, R>, config: SQLiteTransactionConfig | undefined) {
|
||||
if (managesTransactionsNatively(this.client)) return this.client.withTransaction(effect)
|
||||
return Effect.uninterruptibleMask((restore) =>
|
||||
Effect.withFiber<A, E | SqlError, R>((fiber) => {
|
||||
const services = fiber.context
|
||||
|
||||
+2
@@ -41,6 +41,7 @@ import m38 from "./migration/20260804233008_loose_psylocke.js"
|
||||
import m39 from "./migration/20260805200742_import_legacy_credentials.js"
|
||||
import m40 from "./migration/20260808023530_workspace_domain.js"
|
||||
import m41 from "./migration/20260811161259_execution_claim_attempts.js"
|
||||
import m42 from "./migration/20260812181746_session_inbox.js"
|
||||
|
||||
export const migrations = [
|
||||
m00,
|
||||
@@ -85,4 +86,5 @@ export const migrations = [
|
||||
m39,
|
||||
m40,
|
||||
m41,
|
||||
m42,
|
||||
] satisfies DatabaseMigration.Migration[]
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration.js"
|
||||
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260812181746_session_inbox",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`
|
||||
CREATE TABLE \`session_inbox\` (
|
||||
\`id\` text PRIMARY KEY,
|
||||
\`session_id\` text NOT NULL,
|
||||
\`type\` text NOT NULL,
|
||||
\`payload\` text NOT NULL,
|
||||
\`delivery\` text NOT NULL,
|
||||
\`enqueued_seq\` integer NOT NULL,
|
||||
\`time_created\` integer NOT NULL,
|
||||
CONSTRAINT \`fk_session_inbox_session_id_session_v2_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session_v2\`(\`id\`) ON DELETE CASCADE
|
||||
);
|
||||
`)
|
||||
yield* tx.run(
|
||||
`CREATE INDEX \`session_inbox_session_delivery_seq_idx\` ON \`session_inbox\` (\`session_id\`,\`delivery\`,\`enqueued_seq\`);`,
|
||||
)
|
||||
yield* tx.run(
|
||||
`CREATE UNIQUE INDEX \`session_inbox_session_enqueued_seq_idx\` ON \`session_inbox\` (\`session_id\`,\`enqueued_seq\`);`,
|
||||
)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
export default migration
|
||||
@@ -142,6 +142,18 @@ const schema: Omit<DatabaseMigration.Migration, "id"> = {
|
||||
CONSTRAINT \`fk_instruction_state_session_id_session_v2_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session_v2\`(\`id\`) ON DELETE CASCADE
|
||||
);
|
||||
`)
|
||||
yield* tx.run(`
|
||||
CREATE TABLE \`session_inbox\` (
|
||||
\`id\` text PRIMARY KEY,
|
||||
\`session_id\` text NOT NULL,
|
||||
\`type\` text NOT NULL,
|
||||
\`payload\` text NOT NULL,
|
||||
\`delivery\` text NOT NULL,
|
||||
\`enqueued_seq\` integer NOT NULL,
|
||||
\`time_created\` integer NOT NULL,
|
||||
CONSTRAINT \`fk_session_inbox_session_id_session_v2_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session_v2\`(\`id\`) ON DELETE CASCADE
|
||||
);
|
||||
`)
|
||||
yield* tx.run(`
|
||||
CREATE TABLE \`session_message\` (
|
||||
\`id\` text PRIMARY KEY,
|
||||
@@ -218,6 +230,12 @@ const schema: Omit<DatabaseMigration.Migration, "id"> = {
|
||||
yield* tx.run(
|
||||
`CREATE UNIQUE INDEX \`permission_project_action_resource_idx\` ON \`permission\` (\`project_id\`,\`action\`,\`resource\`);`,
|
||||
)
|
||||
yield* tx.run(
|
||||
`CREATE INDEX \`session_inbox_session_delivery_seq_idx\` ON \`session_inbox\` (\`session_id\`,\`delivery\`,\`enqueued_seq\`);`,
|
||||
)
|
||||
yield* tx.run(
|
||||
`CREATE UNIQUE INDEX \`session_inbox_session_enqueued_seq_idx\` ON \`session_inbox\` (\`session_id\`,\`enqueued_seq\`);`,
|
||||
)
|
||||
yield* tx.run(
|
||||
`CREATE UNIQUE INDEX \`session_message_session_seq_idx\` ON \`session_message\` (\`session_id\`,\`seq\`);`,
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Reactivity } from "effect/unstable/reactivity"
|
||||
import { SqlClient, Statement } from "effect/unstable/sql"
|
||||
import type { Connection } from "effect/unstable/sql/SqlConnection"
|
||||
import { classifySqliteError, SqlError, UnknownError } from "effect/unstable/sql/SqlError"
|
||||
import type { NativeTransactionSqlClient } from "./drizzle/effect-sqlite/session.js"
|
||||
import { Sqlite } from "./sqlite.js"
|
||||
|
||||
const ATTR_DB_SYSTEM_NAME = "db.system.name"
|
||||
@@ -215,10 +216,10 @@ const make = (options: Config) =>
|
||||
config: options,
|
||||
withTransaction: makeWithTransaction(native, connection, semaphore),
|
||||
// Durable Object SQLite rejects BEGIN/COMMIT/SAVEPOINT; consumers such
|
||||
// as the drizzle session must route through withTransaction instead.
|
||||
// as the drizzle session detect this and route through withTransaction.
|
||||
transactionStatements: false,
|
||||
},
|
||||
)
|
||||
} as const,
|
||||
) satisfies NativeTransactionSqlClient
|
||||
|
||||
return client
|
||||
})
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Effect, Layer, PlatformError } from "effect"
|
||||
import { ChildProcessSpawner, make } from "effect/unstable/process/ChildProcessSpawner"
|
||||
|
||||
export const spawner = make(() =>
|
||||
Effect.fail(
|
||||
PlatformError.systemError({
|
||||
_tag: "Unknown",
|
||||
module: "Environment",
|
||||
method: "spawn",
|
||||
description: "This location has no execution plane: no workspace is attached and the host cannot spawn processes",
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
export const layer = Layer.succeed(ChildProcessSpawner, spawner)
|
||||
|
||||
export * as EnvironmentUnavailable from "./unavailable.js"
|
||||
@@ -0,0 +1,12 @@
|
||||
import { bind } from "./fff.js"
|
||||
|
||||
export type { Directory, DirSearch, File, Init, Mixed, MixedSearch, Picker, Result, Search } from "./fff.js"
|
||||
|
||||
// No fff backend exists on workerd; every create reports unavailability and
|
||||
// FileSystemSearch degrades the same way it does on a runtime without the native module.
|
||||
const adapter = bind(undefined, "fff unavailable on workerd runtime")
|
||||
|
||||
export const available = adapter.available
|
||||
export const create = adapter.create
|
||||
|
||||
export * as Fff from "./fff.workerd.js"
|
||||
@@ -2,9 +2,10 @@ import { createRequire } from "node:module"
|
||||
|
||||
declare const OPENCODE_LIBC: string | undefined
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
|
||||
// Lazy: on workerd import.meta.url is undefined and the watcher is never
|
||||
// loaded, so createRequire must not run at module scope.
|
||||
export default function load() {
|
||||
const require = createRequire(import.meta.url)
|
||||
const libc = typeof OPENCODE_LIBC === "undefined" ? undefined : OPENCODE_LIBC
|
||||
return require(
|
||||
process.env.OPENCODE_PARCEL_WATCHER_PATH ??
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
// workerd has no filesystem path to a photon wasm artifact. Image.Photon only
|
||||
// reads this lazily and surfaces a typed ResizerUnavailableError when loading
|
||||
// fails, so an empty path degrades cleanly instead of breaking module load.
|
||||
export default ""
|
||||
@@ -1,11 +1,10 @@
|
||||
export * as MCPClient from "./client.js"
|
||||
|
||||
import path from "node:path"
|
||||
import { execFile } from "node:child_process"
|
||||
import { pathToFileURL } from "node:url"
|
||||
import { Client } from "@modelcontextprotocol/sdk/client/index.js"
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
|
||||
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
|
||||
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"
|
||||
import { UnauthorizedError, type OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js"
|
||||
import {
|
||||
CallToolResultSchema,
|
||||
@@ -30,13 +29,12 @@ import {
|
||||
} from "@modelcontextprotocol/sdk/types.js"
|
||||
import { Cause, Effect, Exit, Schema } from "effect"
|
||||
import { ConfigMCP } from "@opencode-ai/schema/config/mcp"
|
||||
import { MCPStdio } from "./stdio.js"
|
||||
|
||||
const DEFAULT_STARTUP_TIMEOUT = 30_000
|
||||
const DEFAULT_CATALOG_TIMEOUT = 30_000
|
||||
const DEFAULT_EXECUTION_TIMEOUT = 12 * 60 * 60 * 1_000 // 12 hours
|
||||
|
||||
type Transport = StdioClientTransport | StreamableHTTPClientTransport
|
||||
|
||||
// Some servers advertise tool outputSchemas the SDK's strict validator can't resolve; this drops
|
||||
// only that field so a single bad schema doesn't blank out the whole tool list.
|
||||
const TolerantListToolsResult = ListToolsResultSchema.extend({
|
||||
@@ -176,7 +174,12 @@ export interface Connection {
|
||||
readonly onResourcesChanged: (callback: () => void) => void
|
||||
}
|
||||
|
||||
/** Connects an MCP server; closing the calling scope tears down the transport and any spawned process. */
|
||||
/**
|
||||
* Connects an MCP server; closing the calling scope tears down the transport and any spawned process.
|
||||
*
|
||||
* A stdio server is spawned through the location's `Environment`, so it runs on the same execution
|
||||
* plane as the location's shell commands rather than always on the host.
|
||||
*/
|
||||
export const connect = Effect.fnUntraced(function* (
|
||||
server: string,
|
||||
config: typeof ConfigMCP.Server.Type,
|
||||
@@ -190,13 +193,12 @@ export const connect = Effect.fnUntraced(function* (
|
||||
const transport: Transport = yield* Effect.gen(function* () {
|
||||
if (config.type === "local") {
|
||||
const [command, ...args] = config.command
|
||||
return new StdioClientTransport({
|
||||
return yield* MCPStdio.make({
|
||||
server,
|
||||
command,
|
||||
args,
|
||||
cwd: config.cwd ? path.resolve(directory, config.cwd) : directory,
|
||||
stderr: "pipe",
|
||||
env: {
|
||||
...(process.env as Record<string, string>),
|
||||
environment: {
|
||||
...(command === "opencode" ? { BUN_BE_BUN: "1" } : {}),
|
||||
...config.environment,
|
||||
},
|
||||
@@ -233,9 +235,9 @@ export const connect = Effect.fnUntraced(function* (
|
||||
catch: (error) => error,
|
||||
}).pipe(Effect.exit)
|
||||
if (Exit.isSuccess(exit)) {
|
||||
yield* Effect.addFinalizer(() =>
|
||||
cleanupStdioDescendants(transport).pipe(Effect.andThen(Effect.promise(() => client.close())), Effect.ignore),
|
||||
)
|
||||
// Closing the client closes the transport, which ends stdin and then kills through the spawner
|
||||
// handle if the server does not exit cleanly. The process scope remains a final backstop.
|
||||
yield* Effect.addFinalizer(() => Effect.promise(() => client.close()).pipe(Effect.ignore))
|
||||
const catalogTimeout = config.timeout?.catalog ?? DEFAULT_CATALOG_TIMEOUT
|
||||
const executionTimeout = config.timeout?.execution ?? DEFAULT_EXECUTION_TIMEOUT
|
||||
return {
|
||||
@@ -434,58 +436,12 @@ export const connect = Effect.fnUntraced(function* (
|
||||
} satisfies Connection
|
||||
}
|
||||
|
||||
yield* cleanupStdioDescendants(transport).pipe(Effect.andThen(Effect.promise(() => transport.close())), Effect.ignore)
|
||||
yield* Effect.promise(() => transport.close()).pipe(Effect.ignore)
|
||||
const error = Cause.squash(exit.cause)
|
||||
if (error instanceof UnauthorizedError) return yield* new NeedsAuthError({ server })
|
||||
return yield* new ConnectError({ server, message: error instanceof Error ? error.message : String(error) })
|
||||
})
|
||||
|
||||
// SDK close stops the MCP process, but not child processes it spawned.
|
||||
const cleanupStdioDescendants = (transport: Transport) =>
|
||||
Effect.gen(function* () {
|
||||
if (!(transport instanceof StdioClientTransport)) return
|
||||
const pid = transport.pid
|
||||
if (typeof pid !== "number") return
|
||||
yield* Effect.forEach(
|
||||
yield* descendantPids(pid),
|
||||
(pid) =>
|
||||
Effect.try({
|
||||
try: () => process.kill(pid, "SIGTERM"),
|
||||
catch: () => undefined,
|
||||
}).pipe(Effect.ignore),
|
||||
{ discard: true },
|
||||
)
|
||||
})
|
||||
|
||||
const descendantPids = Effect.fnUntraced(function* (root: number) {
|
||||
if (process.platform === "win32") return []
|
||||
const result: number[] = []
|
||||
const queue = [root]
|
||||
for (let index = 0; index < queue.length; index++) {
|
||||
const parent = queue[index]
|
||||
if (parent === undefined) return result
|
||||
const children = (yield* childPids(parent)).filter((pid) => !result.includes(pid))
|
||||
result.push(...children)
|
||||
queue.push(...children)
|
||||
}
|
||||
return result
|
||||
})
|
||||
|
||||
const childPids = (pid: number) =>
|
||||
Effect.promise(
|
||||
() =>
|
||||
new Promise<number[]>((resolve) => {
|
||||
execFile("pgrep", ["-P", String(pid)], { encoding: "utf8" }, (_error, stdout) => {
|
||||
resolve(
|
||||
stdout
|
||||
.split("\n")
|
||||
.map((line) => Number.parseInt(line, 10))
|
||||
.filter((pid) => Number.isInteger(pid)),
|
||||
)
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
async function paginate<R extends { nextCursor?: string }, T>(
|
||||
list: (cursor: string | undefined) => Promise<R>,
|
||||
items: (result: R) => T[],
|
||||
|
||||
@@ -12,6 +12,7 @@ import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
|
||||
import { Config } from "../config.js"
|
||||
import { Credential } from "../credential.js"
|
||||
import { Bus } from "../bus.js"
|
||||
import { Environment } from "../environment/index.js"
|
||||
import { Form } from "../form.js"
|
||||
import { Integration } from "../integration.js"
|
||||
import { KeyedMutex } from "../effect/keyed-mutex.js"
|
||||
@@ -173,13 +174,13 @@ export const layer = (options?: Options) =>
|
||||
Effect.gen(function* () {
|
||||
const config = yield* Config.Service
|
||||
const location = yield* Location.Service
|
||||
const environment = yield* Environment.Service
|
||||
const bus = yield* Bus.Service
|
||||
const forms = yield* Form.Service
|
||||
const integration = yield* Integration.Service
|
||||
const credentials = yield* Credential.Service
|
||||
const root = yield* Scope.make()
|
||||
const root = yield* Effect.scope
|
||||
const fork = yield* FiberSet.makeRuntime<never, void, never>()
|
||||
yield* Effect.addFinalizer((exit) => Scope.close(root, exit))
|
||||
|
||||
const loadConfig = (entries: readonly Entry[]) => {
|
||||
const documents = entries.filter((entry): entry is Document => entry.type === "document")
|
||||
@@ -459,13 +460,8 @@ export const layer = (options?: Options) =>
|
||||
connection.onClose(() =>
|
||||
live(
|
||||
Effect.gen(function* () {
|
||||
entry.client = undefined
|
||||
entry.tools = undefined
|
||||
entry.prompts = undefined
|
||||
entry.status = { status: "failed", error: "Connection closed" }
|
||||
yield* bus.publish(McpEvent.ToolsChanged, { server: name }).pipe(Effect.ignore)
|
||||
yield* bus.publish(McpEvent.ResourcesChanged, { server: name }).pipe(Effect.ignore)
|
||||
yield* bus.publish(Command.Event.Updated, {}).pipe(Effect.ignore)
|
||||
yield* stopServer(name, entry)
|
||||
yield* bus.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore)
|
||||
}),
|
||||
),
|
||||
@@ -520,6 +516,8 @@ export const layer = (options?: Options) =>
|
||||
options?.clientInfo,
|
||||
).pipe(
|
||||
Effect.flatMap((connection) => connection.tools().pipe(Effect.map((tools) => ({ connection, tools })))),
|
||||
// A stdio server is spawned on this location's execution plane, not the host's.
|
||||
Effect.provideService(Environment.Service, environment),
|
||||
Scope.provide(scope),
|
||||
Effect.exit,
|
||||
)
|
||||
@@ -828,7 +826,7 @@ export function configured(options?: Options) {
|
||||
return makeLocationNode({
|
||||
service: Service,
|
||||
layer: layer(options),
|
||||
deps: [Config.node, Location.node, Bus.node, Form.node, Integration.node, Credential.node],
|
||||
deps: [Config.node, Location.node, Environment.node, Bus.node, Form.node, Integration.node, Credential.node],
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user