From 8c94e9005ffe55a51aa664a47bbe58bb58b4ddb0 Mon Sep 17 00:00:00 2001 From: James Long Date: Wed, 1 Jul 2026 17:12:00 -0400 Subject: [PATCH] chore: merge dev into v2 (#34788) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Co-authored-by: Kit Langton Co-authored-by: opencode-agent[bot] Co-authored-by: Affan Ali <93028901+affanali2k3@users.noreply.github.com> Co-authored-by: affanali2k3 Co-authored-by: Frank Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com> Co-authored-by: 𝓛𝓲𝓽𝓽𝓵𝓮 𝓕𝓻𝓪𝓷𝓴 Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Co-authored-by: Jay V Co-authored-by: Dax Raad Co-authored-by: Aarav Sareen <96787824+arvsrn@users.noreply.github.com> Co-authored-by: OpeOginni <107570612+OpeOginni@users.noreply.github.com> Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com> Co-authored-by: Ben Guthrie Co-authored-by: Dax Co-authored-by: Filip <34747899+neriousy@users.noreply.github.com> Co-authored-by: Max Anderson Co-authored-by: Brendan Allan Co-authored-by: Jack Co-authored-by: Shoubhit Dash Co-authored-by: Dustin Deus Co-authored-by: starptech Co-authored-by: Aiden Cline Co-authored-by: usrnk1 <7547651+usrnk1@users.noreply.github.com> Co-authored-by: Jay <53023+jayair@users.noreply.github.com> Co-authored-by: runvip <164729189+runvip@users.noreply.github.com> Co-authored-by: opencode Co-authored-by: Julian Coy Co-authored-by: Vladimir Glafirov --- .github/workflows/compliance-close.yml | 38 + .github/workflows/duplicate-issues.yml | 46 +- .github/workflows/triage.yml | 19 + bun.lock | 80 +- nix/hashes.json | 8 +- .../timeline/timeline-test-helpers.ts | 2 +- .../regression/cross-server-tab-close.spec.ts | 4 +- .../regression/prompt-thinking-level.spec.ts | 2 +- .../regression/session-request-docks.spec.ts | 23 + .../session-todo-dock-navigation.spec.ts | 2 +- .../app/e2e/smoke/session-timeline.spec.ts | 4 +- packages/app/package.json | 2 +- packages/app/src/app.tsx | 146 +- .../src/components/dialog-manage-models.tsx | 163 +- .../src/components/dialog-select-model.tsx | 287 +- packages/app/src/components/prompt-input.tsx | 300 +- .../prompt-input/build-request-parts.test.ts | 35 + .../prompt-input/build-request-parts.ts | 34 +- .../components/prompt-input/context-items.tsx | 2 +- .../components/prompt-input/slash-popover.tsx | 307 +- .../components/prompt-workspace-selector.tsx | 16 +- .../src/components/session-context-usage.tsx | 103 +- .../session/session-context-metrics.test.ts | 55 +- .../session/session-context-metrics.ts | 48 +- .../session/session-context-tab.tsx | 18 +- .../src/components/session/session-header.tsx | 1 + .../app/src/components/titlebar-tab-nav.css | 6 +- .../app/src/components/titlebar-tab-nav.tsx | 196 +- .../src/components/titlebar-tab-popover.css | 133 + .../src/components/titlebar-tab-popover.tsx | 99 + .../app/src/components/titlebar-tab-strip.tsx | 7 +- packages/app/src/components/titlebar.tsx | 9 +- .../src/context/global-sync/bootstrap.test.ts | 3 + .../app/src/context/global-sync/bootstrap.ts | 12 +- .../context/global-sync/child-store.test.ts | 8 +- .../src/context/global-sync/child-store.ts | 8 + .../src/context/global-sync/event-reducer.ts | 5 + packages/app/src/context/global-sync/types.ts | 6 + packages/app/src/context/layout.tsx | 44 +- packages/app/src/context/local.tsx | 71 +- packages/app/src/context/platform.tsx | 3 + packages/app/src/context/prompt.tsx | 13 +- packages/app/src/context/server-sync.tsx | 23 +- packages/app/src/context/tabs.tsx | 18 +- packages/app/src/desktop-menu.test.ts | 13 + packages/app/src/i18n/en.ts | 24 + packages/app/src/index.css | 157 + packages/app/src/pages/error.tsx | 5 +- packages/app/src/pages/home.tsx | 336 +- packages/app/src/pages/layout.tsx | 12 - .../src/pages/layout/session-tab-avatar.tsx | 36 +- packages/app/src/pages/session.tsx | 405 ++- .../composer/session-composer-region.tsx | 2 +- .../composer/session-question-dock.tsx | 341 +- .../session/timeline/message-timeline.tsx | 474 ++- packages/app/src/utils/persist.ts | 24 +- packages/app/src/wsl/add-server-probes.ts | 57 + packages/app/src/wsl/dialog-add-server.tsx | 936 +++-- .../app/src/wsl/dialog-add-wsl-server.css | 392 +++ packages/app/src/wsl/settings-model.test.ts | 192 +- packages/app/src/wsl/settings-model.ts | 342 +- packages/app/src/wsl/settings.tsx | 14 +- packages/app/src/wsl/types.ts | 6 +- packages/cli/package.json | 2 +- packages/cli/src/commands/handlers/serve.ts | 5 +- packages/cli/src/index.ts | 9 +- packages/cli/src/services/daemon.ts | 2 - packages/cli/src/services/standalone.ts | 3 +- packages/cli/src/services/updater.ts | 2 - packages/cli/src/tui.ts | 3 +- .../client/test/contract-identity.test.ts | 6 +- packages/console/app/package.json | 2 +- packages/console/app/src/i18n/ar.ts | 6 +- packages/console/app/src/i18n/br.ts | 6 +- packages/console/app/src/i18n/da.ts | 6 +- packages/console/app/src/i18n/de.ts | 6 +- packages/console/app/src/i18n/en.ts | 6 +- packages/console/app/src/i18n/es.ts | 6 +- packages/console/app/src/i18n/fr.ts | 6 +- packages/console/app/src/i18n/it.ts | 6 +- packages/console/app/src/i18n/ja.ts | 6 +- packages/console/app/src/i18n/ko.ts | 6 +- packages/console/app/src/i18n/no.ts | 6 +- packages/console/app/src/i18n/pl.ts | 6 +- packages/console/app/src/i18n/ru.ts | 6 +- packages/console/app/src/i18n/th.ts | 6 +- packages/console/app/src/i18n/tr.ts | 6 +- packages/console/app/src/i18n/uk.ts | 6 +- packages/console/app/src/i18n/zh.ts | 5 +- packages/console/app/src/i18n/zht.ts | 5 +- .../console/app/src/lib/request-country.ts | 5 + packages/console/app/src/routes/go/index.css | 35 - packages/console/app/src/routes/go/index.tsx | 28 +- .../app/src/routes/honeycomb/webhook.ts | 2 +- .../workspace/[id]/go/lite-section.module.css | 34 + .../routes/workspace/[id]/go/lite-section.tsx | 49 + .../[id]/settings/settings-section.tsx | 1 + .../console/app/src/routes/zen/util/error.ts | 1 + .../app/src/routes/zen/util/handler.ts | 192 +- .../src/routes/zen/util/modelTpsLimiter.ts | 2 +- .../routes/zen/util/providerBudgetTracker.ts | 140 +- .../migration.sql | 1 + .../snapshot.json | 3093 +++++++++++++++++ packages/console/core/package.json | 2 +- packages/console/core/src/model.ts | 2 +- .../console/core/src/schema/workspace.sql.ts | 3 +- packages/console/core/src/workspace.ts | 28 +- packages/console/function/package.json | 2 +- packages/console/mail/package.json | 2 +- packages/console/support/package.json | 2 +- packages/core/package.json | 6 +- packages/core/src/agent.ts | 4 +- packages/core/src/aisdk.ts | 6 +- packages/core/src/catalog.ts | 7 +- packages/core/src/command.ts | 4 +- packages/core/src/config.ts | 4 +- .../core/src/control-plane/move-session.ts | 14 +- packages/core/src/credential.ts | 4 +- packages/core/src/cross-spawn-spawner.ts | 3 +- packages/core/src/database/database.ts | 8 +- packages/core/src/effect/app-node-builder.ts | 23 +- packages/core/src/effect/dfdf | 1 + packages/core/src/effect/layer-node.ts | 172 +- packages/core/src/event.ts | 2 - packages/core/src/file-mutation.ts | 4 +- packages/core/src/filesystem.ts | 4 - packages/core/src/filesystem/search.ts | 2 - packages/core/src/filesystem/watcher.ts | 4 +- packages/core/src/fs-util.ts | 3 +- packages/core/src/git.ts | 3 +- .../convert-to-openai-responses-input.ts | 10 +- .../openai-responses-language-model.ts | 40 +- packages/core/src/global.ts | 3 +- packages/core/src/image.ts | 4 +- packages/core/src/instruction-context.ts | 2 +- packages/core/src/integration.ts | 4 +- packages/core/src/job.ts | 4 +- packages/core/src/location-mutation.ts | 4 +- packages/core/src/location-services.ts | 13 +- packages/core/src/location.ts | 2 +- packages/core/src/mcp/guidance.ts | 2 - packages/core/src/models-dev.ts | 9 +- packages/core/src/npm.ts | 11 +- packages/core/src/observability.ts | 3 + packages/core/src/permission.ts | 4 +- packages/core/src/permission/saved.ts | 4 +- packages/core/src/plugin.ts | 14 +- packages/core/src/plugin/internal.ts | 9 +- packages/core/src/plugin/runtime.ts | 6 +- packages/core/src/policy.ts | 4 +- packages/core/src/process.ts | 3 +- packages/core/src/project.ts | 7 +- packages/core/src/project/copy.ts | 3 +- packages/core/src/project/directories.ts | 3 +- packages/core/src/pty.ts | 4 +- packages/core/src/pty/ticket.ts | 3 +- packages/core/src/question.ts | 4 +- packages/core/src/reference.ts | 4 +- packages/core/src/reference/guidance.ts | 4 +- packages/core/src/repository-cache.ts | 9 +- packages/core/src/ripgrep.ts | 3 +- packages/core/src/ripgrep/binary.ts | 8 +- packages/core/src/session.ts | 12 +- packages/core/src/session/execution/local.ts | 4 +- packages/core/src/session/projector.ts | 3 +- packages/core/src/session/runner/llm.ts | 6 +- packages/core/src/session/runner/model.ts | 4 +- packages/core/src/session/store.ts | 4 +- packages/core/src/session/todo.ts | 4 +- packages/core/src/shell.ts | 2 - packages/core/src/skill.ts | 4 +- packages/core/src/skill/discovery.ts | 8 +- packages/core/src/skill/guidance.ts | 4 +- packages/core/src/snapshot.ts | 4 +- packages/core/src/system-context/builtins.ts | 8 +- packages/core/src/system-context/registry.ts | 2 +- packages/core/src/tool-output-store.ts | 14 +- packages/core/src/tool/apply-patch.ts | 10 +- packages/core/src/tool/builtins.ts | 54 +- packages/core/src/tool/edit.ts | 10 +- packages/core/src/tool/glob.ts | 10 +- packages/core/src/tool/grep.ts | 10 +- packages/core/src/tool/question.ts | 10 +- packages/core/src/tool/read-filesystem.ts | 2 +- packages/core/src/tool/read.ts | 10 +- packages/core/src/tool/registry.ts | 4 +- packages/core/src/tool/skill.ts | 10 +- packages/core/src/tool/todowrite.ts | 10 +- packages/core/src/tool/webfetch.ts | 11 +- packages/core/src/tool/websearch.ts | 13 +- packages/core/src/tool/write.ts | 10 +- packages/core/src/util/effect-flock.ts | 3 +- packages/core/test/agent.test.ts | 9 +- packages/core/test/catalog.test.ts | 32 +- packages/core/test/command.test.ts | 3 +- packages/core/test/config/agent.test.ts | 9 +- packages/core/test/config/command.test.ts | 6 +- packages/core/test/config/config.test.ts | 24 +- packages/core/test/credential.test.ts | 3 +- packages/core/test/database-migration.test.ts | 5 +- .../layer-node/layer-node-types.test.ts | 24 +- .../test/effect/layer-node/layer-node.test.ts | 61 +- .../test/effect/layer-node/node-build.test.ts | 50 +- packages/core/test/event.test.ts | 13 +- packages/core/test/file-mutation.test.ts | 15 +- .../core/test/filesystem/filesystem.test.ts | 7 +- packages/core/test/filesystem/watcher.test.ts | 21 +- .../core/test/fixture/effect-flock-worker.ts | 6 +- .../openai-responses-language-model.test.ts | 206 ++ .../core/test/instruction-context.test.ts | 148 +- packages/core/test/job.test.ts | 3 +- .../core/test/location-filesystem.test.ts | 19 +- packages/core/test/location-layer.test.ts | 29 +- packages/core/test/location-mutation.test.ts | 19 +- packages/core/test/location.test.ts | 3 +- packages/core/test/models.test.ts | 15 +- packages/core/test/move-session.test.ts | 50 +- packages/core/test/npm.test.ts | 13 +- packages/core/test/permission.test.ts | 34 +- packages/core/test/plugin/command.test.ts | 11 +- packages/core/test/plugin/fixture.ts | 73 +- packages/core/test/plugin/models-dev.test.ts | 19 +- .../core/test/plugin/provider-dynamic.test.ts | 3 +- .../test/plugin/provider-opencode.test.ts | 7 +- packages/core/test/plugin/skill.test.ts | 4 +- packages/core/test/plugin/variant.test.ts | 18 +- packages/core/test/policy.test.ts | 10 +- packages/core/test/project-copy.test.ts | 14 +- packages/core/test/pty/pty-session.test.ts | 23 +- packages/core/test/pty/ticket.test.ts | 7 +- packages/core/test/reference-guidance.test.ts | 59 +- packages/core/test/reference.test.ts | 27 +- packages/core/test/repository-cache.test.ts | 17 +- packages/core/test/session-compact.test.ts | 26 +- packages/core/test/session-compaction.test.ts | 21 +- packages/core/test/session-create.test.ts | 35 +- packages/core/test/session-history.test.ts | 26 +- packages/core/test/session-projector.test.ts | 9 +- packages/core/test/session-prompt.test.ts | 22 +- .../core/test/session-runner-recorded.test.ts | 100 +- .../test/session-runner-tool-registry.test.ts | 5 +- packages/core/test/session-runner.test.ts | 109 +- packages/core/test/session-title.test.ts | 23 +- .../core/test/session-tool-progress.test.ts | 5 +- packages/core/test/session-wait.test.ts | 25 +- packages/core/test/skill-discovery.test.ts | 15 +- packages/core/test/skill.test.ts | 2 +- packages/core/test/skill/guidance.test.ts | 5 +- packages/core/test/snapshot.test.ts | 23 +- .../core/test/system-context/builtins.test.ts | 18 +- .../core/test/system-context/registry.test.ts | 3 +- packages/core/test/tool-apply-patch.test.ts | 35 +- packages/core/test/tool-edit.test.ts | 35 +- packages/core/test/tool-output-store.test.ts | 24 +- packages/core/test/tool-question.test.ts | 13 +- .../core/test/tool-read-filesystem.test.ts | 7 +- packages/core/test/tool-read.test.ts | 51 +- packages/core/test/tool-shell.test.ts | 32 +- packages/core/test/tool-skill.test.ts | 20 +- packages/core/test/tool-subagent.test.ts | 22 +- packages/core/test/tool-todowrite.test.ts | 24 +- packages/core/test/tool-webfetch.test.ts | 23 +- packages/core/test/tool-websearch.test.ts | 21 +- packages/core/test/tool-write.test.ts | 34 +- packages/core/test/util/effect-flock.test.ts | 7 +- packages/desktop/package.json | 2 +- packages/desktop/src/main/index.ts | 39 +- packages/desktop/src/main/ipc.ts | 14 +- .../desktop/src/main/store-cleanup.test.ts | 93 + packages/desktop/src/main/store-cleanup.ts | 94 + packages/desktop/src/main/store-keys.ts | 1 + packages/desktop/src/main/store.ts | 5 + packages/desktop/src/main/windows.ts | 76 +- packages/desktop/src/main/wsl/ipc.ts | 12 +- packages/desktop/src/main/wsl/policy.ts | 7 + packages/desktop/src/main/wsl/servers.test.ts | 66 +- packages/desktop/src/main/wsl/servers.ts | 44 +- packages/desktop/src/preload/index.ts | 4 +- packages/desktop/src/preload/types.ts | 1 + packages/desktop/src/renderer/index.tsx | 77 +- packages/effect-drizzle-sqlite/package.json | 2 +- packages/effect-sqlite-node/package.json | 2 +- packages/enterprise/package.json | 2 +- packages/function/package.json | 2 +- packages/http-recorder/package.json | 2 +- .../httpapi-codegen/test/generate.test.ts | 16 + packages/llm/example/tutorial.ts | 2 +- packages/llm/package.json | 2 +- .../llm/src/protocols/anthropic-messages.ts | 18 +- .../llm/src/protocols/bedrock-converse.ts | 17 +- packages/llm/src/protocols/gemini.ts | 17 +- packages/llm/src/protocols/openai-chat.ts | 14 +- .../llm/src/protocols/openai-responses.ts | 14 +- packages/llm/src/protocols/shared.ts | 35 +- .../src/protocols/utils/gemini-tool-schema.ts | 4 +- .../llm/src/protocols/utils/tool-schema.ts | 86 + packages/llm/src/provider.ts | 3 +- packages/llm/src/route/client.ts | 36 +- packages/llm/src/route/executor.ts | 2 +- packages/llm/src/route/transport/http.ts | 47 + packages/llm/src/schema/events.ts | 248 +- packages/llm/src/schema/options.ts | 57 +- packages/llm/test/adapter.test.ts | 11 +- ...nttokencount-on-identical-second-call.json | 4 +- packages/llm/test/llm.test.ts | 36 +- packages/llm/test/prepare.test.ts | 178 + .../test/provider/anthropic-messages.test.ts | 4 + .../llm/test/provider/openai-chat.test.ts | 19 +- .../test/provider/openai-responses.test.ts | 5 + packages/llm/test/response.test.ts | 98 + .../llm/test/tool-schema-projection.test.ts | 117 + packages/opencode/package.json | 6 +- packages/opencode/src/account/account.ts | 4 +- packages/opencode/src/account/repo.ts | 4 +- packages/opencode/src/acp/directory.ts | 18 +- packages/opencode/src/acp/service.ts | 12 +- packages/opencode/src/acp/session.ts | 5 +- packages/opencode/src/acp/usage.ts | 19 +- packages/opencode/src/agent/agent.ts | 11 +- packages/opencode/src/auth/index.ts | 4 +- packages/opencode/src/cli/cmd/debug/scrap.ts | 3 +- packages/opencode/src/cli/cmd/mcp.ts | 2 +- packages/opencode/src/cli/cmd/run.ts | 24 +- .../src/cli/cmd/run/variant.shared.ts | 5 +- packages/opencode/src/cli/cmd/tui.ts | 19 +- packages/opencode/src/cli/network.ts | 24 +- packages/opencode/src/cli/tui/layer.ts | 3 +- packages/opencode/src/command/index.ts | 8 +- packages/opencode/src/config/config.ts | 12 +- packages/opencode/src/config/tui.ts | 8 +- .../opencode/src/control-plane/workspace.ts | 21 +- .../src/effect/app-node-builder-v1.ts | 12 + packages/opencode/src/effect/app-runtime.ts | 109 +- .../opencode/src/effect/bootstrap-runtime.ts | 12 +- .../opencode/src/effect/config-service.ts | 12 +- packages/opencode/src/effect/runtime-flags.ts | 6 +- packages/opencode/src/env/index.ts | 4 +- packages/opencode/src/event-v2-bridge.ts | 4 +- packages/opencode/src/format/index.ts | 8 +- packages/opencode/src/git/index.ts | 4 +- packages/opencode/src/image/image.ts | 4 +- packages/opencode/src/installation/index.ts | 11 +- packages/opencode/src/job.ts | 2 - packages/opencode/src/lsp/lsp.ts | 8 +- packages/opencode/src/mcp/auth.ts | 4 +- packages/opencode/src/mcp/catalog.ts | 3 +- packages/opencode/src/mcp/index.ts | 14 +- packages/opencode/src/permission/index.ts | 4 +- packages/opencode/src/plugin/index.ts | 8 +- packages/opencode/src/plugin/tui/runtime.ts | 3 +- packages/opencode/src/project/bootstrap.ts | 19 +- .../opencode/src/project/instance-layer.ts | 11 - .../opencode/src/project/instance-store.ts | 10 +- packages/opencode/src/project/project.ts | 13 +- packages/opencode/src/project/vcs.ts | 4 +- packages/opencode/src/provider/auth.ts | 6 +- packages/opencode/src/provider/provider.ts | 14 +- packages/opencode/src/provider/transform.ts | 32 +- packages/opencode/src/question/index.ts | 4 +- .../server/routes/instance/httpapi/server.ts | 28 +- .../instance/httpapi/websocket-tracker.ts | 5 +- packages/opencode/src/server/server.ts | 3 +- packages/opencode/src/session/compaction.ts | 15 +- packages/opencode/src/session/instruction.ts | 10 +- packages/opencode/src/session/llm.ts | 19 +- packages/opencode/src/session/processor.ts | 19 +- packages/opencode/src/session/prompt.ts | 36 +- packages/opencode/src/session/revert.ts | 13 +- packages/opencode/src/session/run-state.ts | 4 +- packages/opencode/src/session/session.ts | 90 +- packages/opencode/src/session/status.ts | 4 +- packages/opencode/src/session/summary.ts | 11 +- packages/opencode/src/session/system.ts | 8 +- packages/opencode/src/session/todo.ts | 4 +- packages/opencode/src/share/session.ts | 9 +- packages/opencode/src/share/share-next.ts | 12 +- packages/opencode/src/skill/discovery.ts | 8 +- packages/opencode/src/skill/index.ts | 11 +- packages/opencode/src/snapshot/index.ts | 8 +- packages/opencode/src/storage/storage.ts | 4 +- packages/opencode/src/tool/registry.ts | 30 +- packages/opencode/src/tool/truncate.ts | 4 +- packages/opencode/src/worktree/index.ts | 15 +- packages/opencode/test/account/repo.test.ts | 6 +- .../opencode/test/account/service.test.ts | 9 +- packages/opencode/test/acp/directory.test.ts | 10 +- packages/opencode/test/acp/event.test.ts | 3 +- packages/opencode/test/acp/permission.test.ts | 3 +- packages/opencode/test/acp/session.test.ts | 3 +- packages/opencode/test/acp/usage.test.ts | 37 +- packages/opencode/test/agent/agent.test.ts | 13 +- .../agent/plan-mode-subagent-bypass.test.ts | 3 +- .../agent/plugin-agent-regression.test.ts | 47 +- packages/opencode/test/auth/auth.test.ts | 8 +- .../test/cli/effect-cmd-instance-als.test.ts | 3 +- .../__snapshots__/help-snapshots.test.ts.snap | 56 +- .../test/cli/run/variant.shared.test.ts | 5 +- .../opencode/test/config/agent-color.test.ts | 6 +- packages/opencode/test/config/config.test.ts | 54 +- packages/opencode/test/config/tui.test.ts | 12 +- .../test/control-plane/workspace.test.ts | 42 +- .../test/effect/app-runtime-logger.test.ts | 8 +- .../test/effect/config-service.test.ts | 8 +- .../test/effect/instance-state.test.ts | 3 +- .../test/effect/runtime-flags.test.ts | 9 +- .../test/filesystem/filesystem.test.ts | 7 +- packages/opencode/test/fixture/fixture.ts | 8 +- packages/opencode/test/fixture/workspace.ts | 34 +- packages/opencode/test/format/format.test.ts | 11 +- packages/opencode/test/image/image.test.ts | 16 +- .../test/installation/installation.test.ts | 16 +- packages/opencode/test/job.test.ts | 3 +- packages/opencode/test/lib/cli-process.ts | 8 +- packages/opencode/test/lsp/index.test.ts | 15 +- packages/opencode/test/lsp/lifecycle.test.ts | 6 +- packages/opencode/test/mcp/auth.test.ts | 16 +- packages/opencode/test/mcp/catalog.test.ts | 47 + packages/opencode/test/mcp/headers.test.ts | 3 +- packages/opencode/test/mcp/lifecycle.test.ts | 3 +- .../test/mcp/oauth-auto-connect.test.ts | 12 +- .../opencode/test/mcp/oauth-browser.test.ts | 9 +- packages/opencode/test/patch/patch.test.ts | 3 +- .../opencode/test/permission-task.test.ts | 3 +- .../opencode/test/permission/next.test.ts | 14 +- .../test/plugin/auth-override.test.ts | 56 +- .../test/plugin/loader-shared.test.ts | 19 +- packages/opencode/test/plugin/trigger.test.ts | 36 +- .../test/plugin/workspace-adapter.test.ts | 53 +- .../test/project/instance-bootstrap.test.ts | 11 +- .../opencode/test/project/instance.test.ts | 7 +- .../test/project/migrate-global.test.ts | 5 +- .../test/project/project-directory.test.ts | 5 +- .../opencode/test/project/project.test.ts | 55 +- packages/opencode/test/project/vcs.test.ts | 9 +- .../test/project/worktree-remove.test.ts | 8 +- .../opencode/test/project/worktree.test.ts | 10 +- .../test/provider/amazon-bedrock.test.ts | 5 +- .../test/provider/digitalocean.test.ts | 3 +- .../test/provider/header-timeout.test.ts | 5 +- .../opencode/test/provider/provider.test.ts | 36 +- .../opencode/test/provider/transform.test.ts | 233 +- .../opencode/test/question/question.test.ts | 14 +- .../test/server/global-session-list.test.ts | 6 +- .../test/server/httpapi-authorization.test.ts | 6 +- .../test/server/httpapi-control-plane.test.ts | 2 +- .../test/server/httpapi-experimental.test.ts | 3 +- .../test/server/httpapi-global.test.ts | 2 +- .../server/httpapi-instance-context.test.ts | 13 +- .../httpapi-promptasync-context.test.ts | 13 +- .../test/server/httpapi-provider.test.ts | 3 +- .../server/httpapi-schema-error-body.test.ts | 3 +- .../opencode/test/server/httpapi-sdk.test.ts | 21 +- .../test/server/httpapi-session.test.ts | 31 +- .../opencode/test/server/httpapi-sync.test.ts | 3 +- .../opencode/test/server/httpapi-ui.test.ts | 25 +- .../server/httpapi-workspace-routing.test.ts | 5 +- .../test/server/httpapi-workspace.test.ts | 19 +- .../server/negative-tokens-regression.test.ts | 3 +- .../opencode/test/server/project-copy.test.ts | 12 +- .../test/server/project-init-git.test.ts | 10 +- .../test/server/session-actions.test.ts | 3 +- .../server/session-diff-missing-patch.test.ts | 3 +- .../opencode/test/server/session-list.test.ts | 22 +- .../test/server/session-messages.test.ts | 3 +- .../test/server/session-select.test.ts | 3 +- .../opencode/test/session/compaction.test.ts | 125 +- .../opencode/test/session/instruction.test.ts | 34 +- .../test/session/llm-native-recorded.test.ts | 40 +- .../opencode/test/session/llm-native.test.ts | 7 +- packages/opencode/test/session/llm.test.ts | 53 +- .../test/session/processor-effect.test.ts | 22 +- packages/opencode/test/session/prompt.test.ts | 154 +- .../test/session/revert-compact.test.ts | 17 +- .../opencode/test/session/session.test.ts | 35 +- .../test/session/snapshot-tool-race.test.ts | 15 +- .../structured-output-integration.test.ts | 8 +- packages/opencode/test/session/system.test.ts | 17 +- .../opencode/test/share/share-next.test.ts | 13 +- .../opencode/test/skill/discovery.test.ts | 5 +- packages/opencode/test/skill/skill.test.ts | 23 +- .../opencode/test/snapshot/snapshot.test.ts | 10 +- .../opencode/test/storage/storage.test.ts | 7 +- .../opencode/test/tool/apply_patch.test.ts | 10 +- packages/opencode/test/tool/edit.test.ts | 10 +- .../test/tool/external-directory.test.ts | 3 +- packages/opencode/test/tool/glob.test.ts | 10 +- packages/opencode/test/tool/grep.test.ts | 10 +- packages/opencode/test/tool/lsp.test.ts | 5 +- packages/opencode/test/tool/question.test.ts | 11 +- packages/opencode/test/tool/read.test.ts | 19 +- packages/opencode/test/tool/registry.test.ts | 20 +- packages/opencode/test/tool/shell.test.ts | 19 +- packages/opencode/test/tool/skill.test.ts | 5 +- packages/opencode/test/tool/task.test.ts | 35 +- .../opencode/test/tool/tool-define.test.ts | 5 +- .../opencode/test/tool/truncation.test.ts | 16 +- packages/opencode/test/tool/webfetch.test.ts | 10 +- packages/opencode/test/tool/write.test.ts | 19 +- packages/plugin/package.json | 2 +- packages/sdk-next/src/opencode.ts | 6 +- packages/sdk/js/package.json | 2 +- packages/sdk/js/src/v2/gen/types.gen.ts | 1 + packages/server/package.json | 2 +- packages/server/src/auth.ts | 4 +- packages/server/src/routes.ts | 13 +- packages/session-ui/package.json | 2 +- .../components/line-comment-annotations.tsx | 5 +- .../markdown-inline-code-kind.test.ts | 14 + .../components/markdown-inline-code-kind.ts | 1915 +++++++++- .../session-ui/src/components/markdown.css | 112 +- .../session-ui/src/components/markdown.tsx | 146 +- .../src/components/message-part.css | 62 +- .../src/components/message-part.tsx | 152 +- .../timeline-playground.stories.tsx | 6 +- .../src/components/tool-status-title.tsx | 23 +- packages/slack/package.json | 2 +- packages/stats/app/package.json | 2 +- packages/stats/core/package.json | 2 +- packages/stats/server/package.json | 2 +- .../app/components/dialog-select-model.tsx | 2 + packages/storybook/.storybook/preview.tsx | 12 + packages/tui/package.json | 2 +- packages/tui/src/app.tsx | 69 +- packages/tui/src/component/prompt/index.tsx | 3 + packages/tui/src/context/args.tsx | 1 + packages/tui/src/context/local.tsx | 8 +- packages/tui/src/context/permission.tsx | 26 + packages/tui/test/app-lifecycle.test.tsx | 5 +- .../tui/test/cli/cmd/tui/sync-fixture.tsx | 17 +- packages/ui/package.json | 2 +- .../ui/src/components/progress-circle.css | 8 +- .../ui/src/components/progress-circle.tsx | 9 +- packages/ui/src/components/tooltip.tsx | 2 + packages/ui/src/v2/components/button-v2.css | 31 +- .../src/v2/components/button-v2.stories.tsx | 11 +- packages/ui/src/v2/components/button-v2.tsx | 2 +- packages/ui/src/v2/components/dialog-v2.css | 3 + packages/ui/src/v2/components/dialog-v2.tsx | 2 + packages/ui/src/v2/components/icon.tsx | 24 + packages/ui/src/v2/components/loader-v2.css | 32 + .../src/v2/components/loader-v2.stories.tsx | 42 + packages/ui/src/v2/components/loader-v2.tsx | 31 + .../src/v2/components/progress-circle-v2.css | 13 + .../src/v2/components/progress-circle-v2.tsx | 52 + packages/ui/src/v2/components/radio-v2.css | 12 +- packages/ui/src/v2/components/tooltip-v2.css | 7 + packages/ui/src/v2/components/tooltip-v2.tsx | 3 +- packages/web/package.json | 2 +- packages/web/src/content/docs/ar/go.mdx | 4 +- packages/web/src/content/docs/ar/zen.mdx | 55 +- packages/web/src/content/docs/bs/go.mdx | 4 +- packages/web/src/content/docs/bs/zen.mdx | 29 +- packages/web/src/content/docs/cli.mdx | 41 +- packages/web/src/content/docs/da/go.mdx | 4 +- packages/web/src/content/docs/da/zen.mdx | 55 +- packages/web/src/content/docs/de/go.mdx | 4 +- packages/web/src/content/docs/de/zen.mdx | 55 +- packages/web/src/content/docs/es/go.mdx | 4 +- packages/web/src/content/docs/es/zen.mdx | 29 +- packages/web/src/content/docs/fr/go.mdx | 4 +- packages/web/src/content/docs/fr/zen.mdx | 29 +- packages/web/src/content/docs/go.mdx | 4 +- packages/web/src/content/docs/it/go.mdx | 4 +- packages/web/src/content/docs/it/zen.mdx | 29 +- packages/web/src/content/docs/ja/go.mdx | 4 +- packages/web/src/content/docs/ja/zen.mdx | 55 +- packages/web/src/content/docs/ko/go.mdx | 4 +- packages/web/src/content/docs/ko/zen.mdx | 55 +- packages/web/src/content/docs/nb/go.mdx | 4 +- packages/web/src/content/docs/nb/zen.mdx | 55 +- packages/web/src/content/docs/permissions.mdx | 20 + packages/web/src/content/docs/pl/go.mdx | 4 +- packages/web/src/content/docs/pl/zen.mdx | 55 +- packages/web/src/content/docs/pt-br/go.mdx | 4 +- packages/web/src/content/docs/pt-br/zen.mdx | 29 +- packages/web/src/content/docs/ru/go.mdx | 4 +- packages/web/src/content/docs/ru/index.mdx | 6 +- packages/web/src/content/docs/ru/zen.mdx | 55 +- packages/web/src/content/docs/th/go.mdx | 4 +- packages/web/src/content/docs/th/zen.mdx | 55 +- packages/web/src/content/docs/tr/go.mdx | 4 +- packages/web/src/content/docs/tr/zen.mdx | 29 +- packages/web/src/content/docs/zen.mdx | 55 +- packages/web/src/content/docs/zh-cn/go.mdx | 4 +- packages/web/src/content/docs/zh-cn/zen.mdx | 55 +- packages/web/src/content/docs/zh-tw/go.mdx | 4 +- packages/web/src/content/docs/zh-tw/zen.mdx | 55 +- patches/@tanstack%2Fvirtual-core@3.17.0.patch | 55 +- script/github/close-issues.ts | 19 + sdks/vscode/package.json | 2 +- 590 files changed, 15772 insertions(+), 5530 deletions(-) create mode 100644 packages/app/src/components/titlebar-tab-popover.css create mode 100644 packages/app/src/components/titlebar-tab-popover.tsx create mode 100644 packages/app/src/desktop-menu.test.ts create mode 100644 packages/app/src/wsl/add-server-probes.ts create mode 100644 packages/app/src/wsl/dialog-add-wsl-server.css create mode 100644 packages/console/app/src/lib/request-country.ts create mode 100644 packages/console/core/migrations/20260626161712_oval_callisto/migration.sql create mode 100644 packages/console/core/migrations/20260626161712_oval_callisto/snapshot.json create mode 100644 packages/core/src/effect/dfdf create mode 100644 packages/core/test/github-copilot/openai-responses-language-model.test.ts create mode 100644 packages/desktop/src/main/store-cleanup.test.ts create mode 100644 packages/desktop/src/main/store-cleanup.ts create mode 100644 packages/llm/src/protocols/utils/tool-schema.ts create mode 100644 packages/llm/test/prepare.test.ts create mode 100644 packages/llm/test/response.test.ts create mode 100644 packages/llm/test/tool-schema-projection.test.ts create mode 100644 packages/opencode/src/effect/app-node-builder-v1.ts delete mode 100644 packages/opencode/src/project/instance-layer.ts create mode 100644 packages/opencode/test/mcp/catalog.test.ts create mode 100644 packages/tui/src/context/permission.tsx create mode 100644 packages/ui/src/v2/components/loader-v2.css create mode 100644 packages/ui/src/v2/components/loader-v2.stories.tsx create mode 100644 packages/ui/src/v2/components/loader-v2.tsx create mode 100644 packages/ui/src/v2/components/progress-circle-v2.css create mode 100644 packages/ui/src/v2/components/progress-circle-v2.tsx diff --git a/.github/workflows/compliance-close.yml b/.github/workflows/compliance-close.yml index 14e68701e57..a83824e5cf6 100644 --- a/.github/workflows/compliance-close.yml +++ b/.github/workflows/compliance-close.yml @@ -34,10 +34,48 @@ jobs: const now = Date.now(); const twoHours = 2 * 60 * 60 * 1000; + const orgMemberAssociations = new Set(['OWNER', 'MEMBER']); + const agentLogin = 'opencode-agent[bot]'; + const { data: file } = await github.rest.repos.getContent({ + owner: context.repo.owner, + repo: context.repo.repo, + path: '.github/TEAM_MEMBERS', + ref: 'dev', + }); + const teamMembers = new Set( + Buffer.from(file.content, 'base64') + .toString() + .split('\n') + .map((line) => line.trim().toLowerCase()) + .filter(Boolean) + ); + + function isExempt(item) { + const login = item.user?.login?.toLowerCase(); + return ( + login === agentLogin || + orgMemberAssociations.has(item.author_association) || + (login && teamMembers.has(login)) + ); + } for (const item of items) { const isPR = !!item.pull_request; const kind = isPR ? 'PR' : 'issue'; + const login = item.user?.login; + + if (isExempt(item)) { + core.info(`Skipping ${kind} #${item.number}; author ${login || 'unknown'} is exempt`); + try { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: item.number, + name: 'needs:compliance', + }); + } catch (e) {} + continue; + } const { data: comments } = await github.rest.issues.listComments({ owner: context.repo.owner, diff --git a/.github/workflows/duplicate-issues.yml b/.github/workflows/duplicate-issues.yml index 4648a2d0c3d..3972247dafc 100644 --- a/.github/workflows/duplicate-issues.yml +++ b/.github/workflows/duplicate-issues.yml @@ -17,12 +17,31 @@ jobs: with: fetch-depth: 1 + - name: Check exempt issue author + id: author + run: | + LOGIN="${{ github.event.issue.user.login }}" + ASSOCIATION="${{ github.event.issue.author_association }}" + + if [ "$LOGIN" = "opencode-agent[bot]" ] || + [ "$ASSOCIATION" = "OWNER" ] || + [ "$ASSOCIATION" = "MEMBER" ] || + grep -qxiF "$LOGIN" .github/TEAM_MEMBERS; then + echo "skip=true" >> "$GITHUB_OUTPUT" + echo "Skipping issue automation for exempt author: $LOGIN ($ASSOCIATION)" + else + echo "skip=false" >> "$GITHUB_OUTPUT" + fi + - uses: ./.github/actions/setup-bun + if: steps.author.outputs.skip != 'true' - name: Install opencode + if: steps.author.outputs.skip != 'true' run: curl -fsSL https://opencode.ai/install | bash - name: Check duplicates and compliance + if: steps.author.outputs.skip != 'true' env: OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -38,6 +57,7 @@ jobs: opencode run -m opencode/claude-sonnet-4-6 "A new issue has been created: Issue number: ${{ github.event.issue.number }} + Issue author association: ${{ github.event.issue.author_association }} Lookup this issue with gh issue view ${{ github.event.issue.number }}. @@ -49,6 +69,8 @@ jobs: Check whether the issue follows our contributing guidelines and issue templates. + If the issue author association is OWNER or MEMBER, skip this compliance check. Do not add the needs:compliance label for organization-owned issues. + This project has three issue templates that every issue MUST use one of: 1. Bug Report - requires a Description field with real content @@ -83,7 +105,7 @@ jobs: Based on your findings, post a SINGLE comment on issue #${{ github.event.issue.number }}. Build the comment as follows: - If the issue is NOT compliant, start the comment with: + If the issue is NOT compliant and the author association is not OWNER or MEMBER, start the comment with: Then explain what needs to be fixed and that they have 2 hours to edit the issue before it is automatically closed. Also add the label needs:compliance to the issue using: gh issue edit ${{ github.event.issue.number }} --add-label needs:compliance @@ -129,12 +151,31 @@ jobs: with: fetch-depth: 1 + - name: Check exempt issue author + id: author + run: | + LOGIN="${{ github.event.issue.user.login }}" + ASSOCIATION="${{ github.event.issue.author_association }}" + + if [ "$LOGIN" = "opencode-agent[bot]" ] || + [ "$ASSOCIATION" = "OWNER" ] || + [ "$ASSOCIATION" = "MEMBER" ] || + grep -qxiF "$LOGIN" .github/TEAM_MEMBERS; then + echo "skip=true" >> "$GITHUB_OUTPUT" + echo "Skipping issue automation for exempt author: $LOGIN ($ASSOCIATION)" + else + echo "skip=false" >> "$GITHUB_OUTPUT" + fi + - uses: ./.github/actions/setup-bun + if: steps.author.outputs.skip != 'true' - name: Install opencode + if: steps.author.outputs.skip != 'true' run: curl -fsSL https://opencode.ai/install | bash - name: Recheck compliance + if: steps.author.outputs.skip != 'true' env: OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -148,9 +189,12 @@ jobs: } run: | opencode run -m opencode/claude-sonnet-4-6 "Issue #${{ github.event.issue.number }} was previously flagged as non-compliant and has been edited. + Issue author association: ${{ github.event.issue.author_association }} Lookup this issue with gh issue view ${{ github.event.issue.number }}. + If the issue author association is OWNER or MEMBER, remove the needs:compliance label if present, delete the previous compliance comment if present, and do not post a new comment. + Re-check whether the issue now follows our contributing guidelines and issue templates. This project has three issue templates that every issue MUST use one of: diff --git a/.github/workflows/triage.yml b/.github/workflows/triage.yml index 27852a12ce4..0350e43877c 100644 --- a/.github/workflows/triage.yml +++ b/.github/workflows/triage.yml @@ -16,13 +16,32 @@ jobs: with: fetch-depth: 1 + - name: Check exempt issue author + id: author + run: | + LOGIN="${{ github.event.issue.user.login }}" + ASSOCIATION="${{ github.event.issue.author_association }}" + + if [ "$LOGIN" = "opencode-agent[bot]" ] || + [ "$ASSOCIATION" = "OWNER" ] || + [ "$ASSOCIATION" = "MEMBER" ] || + grep -qxiF "$LOGIN" .github/TEAM_MEMBERS; then + echo "skip=true" >> "$GITHUB_OUTPUT" + echo "Skipping issue automation for exempt author: $LOGIN ($ASSOCIATION)" + else + echo "skip=false" >> "$GITHUB_OUTPUT" + fi + - name: Setup Bun + if: steps.author.outputs.skip != 'true' uses: ./.github/actions/setup-bun - name: Install opencode + if: steps.author.outputs.skip != 'true' run: curl -fsSL https://opencode.ai/install | bash - name: Triage issue + if: steps.author.outputs.skip != 'true' env: OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/bun.lock b/bun.lock index 5658133b841..ae547d8692c 100644 --- a/bun.lock +++ b/bun.lock @@ -29,7 +29,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.17.11", + "version": "1.17.13", "dependencies": { "@dnd-kit/abstract": "0.5.0", "@dnd-kit/dom": "0.5.0", @@ -92,7 +92,7 @@ }, "packages/cli": { "name": "@opencode-ai/cli", - "version": "1.17.11", + "version": "1.17.13", "bin": { "opencode2": "./bin/opencode2.cjs", }, @@ -144,7 +144,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.17.11", + "version": "1.17.13", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -180,7 +180,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.17.11", + "version": "1.17.13", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -207,7 +207,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.17.11", + "version": "1.17.13", "dependencies": { "@ai-sdk/anthropic": "3.0.82", "@ai-sdk/openai": "3.0.48", @@ -229,7 +229,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.17.11", + "version": "1.17.13", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -253,7 +253,7 @@ }, "packages/console/support": { "name": "@opencode-ai/console-support", - "version": "1.17.11", + "version": "1.17.13", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@opencode-ai/console-core": "workspace:*", @@ -273,7 +273,7 @@ }, "packages/core": { "name": "@opencode-ai/core", - "version": "1.17.11", + "version": "1.17.13", "bin": { "opencode": "./bin/opencode", }, @@ -326,7 +326,7 @@ "drizzle-orm": "catalog:", "effect": "catalog:", "fuzzysort": "3.1.0", - "gitlab-ai-provider": "6.9.3", + "gitlab-ai-provider": "6.10.0", "glob": "13.0.5", "google-auth-library": "10.5.0", "gray-matter": "4.0.3", @@ -339,7 +339,7 @@ "npm-package-arg": "13.0.2", "semver": "^7.6.3", "turndown": "7.2.0", - "venice-ai-sdk-provider": "2.0.2", + "venice-ai-sdk-provider": "2.1.1", "which": "6.0.1", "xdg-basedir": "5.1.0", "zod": "catalog:", @@ -368,7 +368,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.17.11", + "version": "1.17.13", "dependencies": { "@zip.js/zip.js": "2.7.62", "effect": "catalog:", @@ -422,7 +422,7 @@ }, "packages/effect-drizzle-sqlite": { "name": "@opencode-ai/effect-drizzle-sqlite", - "version": "1.17.11", + "version": "1.17.13", "dependencies": { "drizzle-orm": "catalog:", "effect": "catalog:", @@ -436,7 +436,7 @@ }, "packages/effect-sqlite-node": { "name": "@opencode-ai/effect-sqlite-node", - "version": "1.17.11", + "version": "1.17.13", "dependencies": { "effect": "catalog:", }, @@ -448,7 +448,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.17.11", + "version": "1.17.13", "dependencies": { "@hono/standard-validator": "catalog:", "@opencode-ai/core": "workspace:*", @@ -480,7 +480,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.17.11", + "version": "1.17.13", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -496,7 +496,7 @@ }, "packages/http-recorder": { "name": "@opencode-ai/http-recorder", - "version": "1.17.11", + "version": "1.17.13", "dependencies": { "@effect/platform-node": "4.0.0-beta.83", "@effect/platform-node-shared": "4.0.0-beta.83", @@ -527,7 +527,7 @@ }, "packages/llm": { "name": "@opencode-ai/llm", - "version": "1.17.11", + "version": "1.17.13", "dependencies": { "@opencode-ai/schema": "workspace:*", "@smithy/eventstream-codec": "4.2.14", @@ -546,7 +546,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.17.11", + "version": "1.17.13", "bin": { "opencode": "./bin/opencode", }, @@ -619,7 +619,7 @@ "drizzle-orm": "catalog:", "effect": "catalog:", "fuzzysort": "3.1.0", - "gitlab-ai-provider": "6.9.3", + "gitlab-ai-provider": "6.10.0", "glob": "13.0.5", "google-auth-library": "10.5.0", "gray-matter": "4.0.3", @@ -643,7 +643,7 @@ "tree-sitter-powershell": "0.25.10", "turndown": "7.2.0", "ulid": "catalog:", - "venice-ai-sdk-provider": "2.0.2", + "venice-ai-sdk-provider": "2.1.1", "vscode-jsonrpc": "8.2.1", "web-tree-sitter": "0.25.10", "ws": "8.21.0", @@ -677,7 +677,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.17.11", + "version": "1.17.13", "dependencies": { "@ai-sdk/provider": "3.0.8", "@opencode-ai/llm": "workspace:*", @@ -762,7 +762,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.17.11", + "version": "1.17.13", "dependencies": { "cross-spawn": "catalog:", }, @@ -777,7 +777,7 @@ }, "packages/server": { "name": "@opencode-ai/server", - "version": "1.17.11", + "version": "1.17.13", "dependencies": { "@opencode-ai/core": "workspace:*", "@opencode-ai/protocol": "workspace:*", @@ -792,7 +792,7 @@ }, "packages/session-ui": { "name": "@opencode-ai/session-ui", - "version": "1.17.11", + "version": "1.17.13", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/core": "workspace:*", @@ -836,7 +836,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.17.11", + "version": "1.17.13", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -849,7 +849,7 @@ }, "packages/stats/app": { "name": "@opencode-ai/stats-app", - "version": "1.17.11", + "version": "1.17.13", "dependencies": { "@ibm/plex": "6.4.1", "@opencode-ai/stats-core": "workspace:*", @@ -882,7 +882,7 @@ }, "packages/stats/core": { "name": "@opencode-ai/stats-core", - "version": "1.17.11", + "version": "1.17.13", "dependencies": { "@aws-sdk/client-athena": "3.933.0", "@planetscale/database": "1.19.0", @@ -901,7 +901,7 @@ }, "packages/stats/server": { "name": "@opencode-ai/stats-server", - "version": "1.17.11", + "version": "1.17.13", "dependencies": { "@aws-sdk/client-firehose": "3.933.0", "@effect/platform-node": "catalog:", @@ -942,7 +942,7 @@ }, "packages/tui": { "name": "@opencode-ai/tui", - "version": "1.17.11", + "version": "1.17.13", "dependencies": { "@opencode-ai/client": "workspace:*", "@opencode-ai/core": "workspace:*", @@ -971,7 +971,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.17.11", + "version": "1.17.13", "dependencies": { "@kobalte/core": "catalog:", "@pierre/diffs": "catalog:", @@ -1022,7 +1022,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.17.11", + "version": "1.17.13", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", @@ -3828,7 +3828,7 @@ "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], - "gitlab-ai-provider": ["gitlab-ai-provider@6.9.3", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=3.0.0", "@ai-sdk/provider-utils": ">=4.0.0" } }, "sha512-lWo6b6es5+k9iXaDIvE9ECzyK4zfEza4+dQ5FN8SJpEuVRi3ZBCpHIOTa32QoYEDCBaiPh+tcyca86PfNodmlg=="], + "gitlab-ai-provider": ["gitlab-ai-provider@6.10.0", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=3.0.0", "@ai-sdk/provider-utils": ">=4.0.0" } }, "sha512-oWEZ06rDO6JjB7INHO882wyBAQqCZVHiDHwCs5M+VPmdDj8TzhGXcYesA2CcV5RoI5lfHLKwGp5uKFB62VWpqw=="], "glob": ["glob@13.0.5", "", { "dependencies": { "minimatch": "^10.2.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-BzXxZg24Ibra1pbQ/zE7Kys4Ua1ks7Bn6pKLkVPZ9FZe4JQS6/Q7ef3LG1H+k7lUf5l4T3PLSyYyYJVYUvfgTw=="], @@ -5446,7 +5446,7 @@ "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], - "venice-ai-sdk-provider": ["venice-ai-sdk-provider@2.0.2", "", { "dependencies": { "@ai-sdk/openai-compatible": "^2.0.47", "@ai-sdk/provider": "^3.0.10", "@ai-sdk/provider-utils": "^4.0.27" }, "peerDependencies": { "ai": "^6.0.90" } }, "sha512-aoa05nI3BTK5aGbjBflq+Gfln2AHAkwNbWuGGvCzUIsOfp5Y3iPD4O4PUGDAEiWVJWbjpPn0KfDa0H/HebwsaA=="], + "venice-ai-sdk-provider": ["venice-ai-sdk-provider@2.1.1", "", { "dependencies": { "@ai-sdk/openai-compatible": "^2.0.51", "@ai-sdk/provider": "^3.0.10", "@ai-sdk/provider-utils": "^4.0.30" }, "peerDependencies": { "ai": "^6.0.90" } }, "sha512-w3OHkuzzKZ3r2TOxER6myBYzZJNoDqol+DUHu3NnfBN/GETnUVxecZJab0CHQQ8GZc0jjzpFymepjcLDPS4SQg=="], "verror": ["verror@1.10.1", "", { "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", "extsprintf": "^1.2.0" } }, "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg=="], @@ -5998,6 +5998,8 @@ "@opencode-ai/core/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="], + "@opencode-ai/core/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + "@opencode-ai/desktop/@actions/artifact": ["@actions/artifact@4.0.0", "", { "dependencies": { "@actions/core": "^1.10.0", "@actions/github": "^6.0.1", "@actions/http-client": "^2.1.0", "@azure/core-http": "^3.0.5", "@azure/storage-blob": "^12.15.0", "@octokit/core": "^5.2.1", "@octokit/plugin-request-log": "^1.0.4", "@octokit/plugin-retry": "^3.0.9", "@octokit/request": "^8.4.1", "@octokit/request-error": "^5.1.1", "@protobuf-ts/plugin": "^2.2.3-alpha.1", "archiver": "^7.0.1", "jwt-decode": "^3.1.2", "unzip-stream": "^0.3.1" } }, "sha512-HCc2jMJRAfviGFAh0FsOR/jNfWhirxl7W6z8zDtttt0GltwxBLdEIjLiweOPFl9WbyJRW1VWnPUSAixJqcWUMQ=="], "@opencode-ai/desktop/marked": ["marked@15.0.12", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA=="], @@ -6008,6 +6010,8 @@ "@opencode-ai/llm/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], + "@opencode-ai/script/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + "@opencode-ai/session-ui/@solid-primitives/resize-observer": ["@solid-primitives/resize-observer@2.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/rootless": "^1.5.2", "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-zBLje5E06TgOg93S7rGPldmhDnouNGhvfZVKOp+oG2XU8snA+GoCSSCz1M+jpNAg5Ek2EakU5UVQqL152WmdXQ=="], "@opencode-ai/ui/@solid-primitives/resize-observer": ["@solid-primitives/resize-observer@2.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/rootless": "^1.5.2", "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-zBLje5E06TgOg93S7rGPldmhDnouNGhvfZVKOp+oG2XU8snA+GoCSSCz1M+jpNAg5Ek2EakU5UVQqL152WmdXQ=="], @@ -6346,6 +6350,8 @@ "opencode/minimatch": ["minimatch@10.0.3", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw=="], + "opencode/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + "opencode-gitlab-auth/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], "openid-client/jose": ["jose@4.15.9", "", {}, "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA=="], @@ -6448,11 +6454,11 @@ "unzipper/fs-extra": ["fs-extra@11.3.5", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg=="], - "venice-ai-sdk-provider/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.47", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Enm5UlL0zUCrW3792opk5h7hRWxZOZzDe6eQYVFqX9LUOGGCe1h8MZWAGim765nwzgnjlpeYOsuzZmLtRsTPlg=="], + "venice-ai-sdk-provider/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.53", "", { "dependencies": { "@ai-sdk/provider": "3.0.12", "@ai-sdk/provider-utils": "4.0.32" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-SoPSkrL5cbNQnAljRsJ7pOzJ2FmWgnhC0lfFOda873ycCdFJL1A+h3Ib7mX2spcv3XnNaO13y/45/0RyqNWlIQ=="], "venice-ai-sdk-provider/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], - "venice-ai-sdk-provider/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="], + "venice-ai-sdk-provider/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.32", "", { "dependencies": { "@ai-sdk/provider": "3.0.12", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Kwj499fTcN9bP/AfGoPU7JWIXeP6VZqKI6omsH062c9E2G4gdjeJczkz4z/tYSkzYjLE2AI3DtZbMfs6D7vn2Q=="], "verror/core-util-is": ["core-util-is@1.0.2", "", {}, "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="], @@ -7066,6 +7072,10 @@ "unzipper/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], + "venice-ai-sdk-provider/@ai-sdk/openai-compatible/@ai-sdk/provider": ["@ai-sdk/provider@3.0.12", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-sj9DWTJ2Ze0WR9qsiOPqoqzNx3OxL6iMxHImbhvoe9qOspekbzxNDMiJ4TIGfYHYh9w4OmBjz3prvqhzTi96+Q=="], + + "venice-ai-sdk-provider/@ai-sdk/provider-utils/@ai-sdk/provider": ["@ai-sdk/provider@3.0.12", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-sj9DWTJ2Ze0WR9qsiOPqoqzNx3OxL6iMxHImbhvoe9qOspekbzxNDMiJ4TIGfYHYh9w4OmBjz3prvqhzTi96+Q=="], + "venice-ai-sdk-provider/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "vitest/@vitest/expect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], diff --git a/nix/hashes.json b/nix/hashes.json index 5809bf7ff3d..4571c208223 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-JXQ9PAqqRlJtHa8T3ZxqdRRyxC+0ip+2wSnehvKXUbI=", - "aarch64-linux": "sha256-nI+RaxDXmAcjhSjCtIvyi292xBEg0E5NXaoyGrJE69s=", - "aarch64-darwin": "sha256-UleKpm7Khf3kibm4BqZJ9bmu0N2kOjNd77g1Bd2tmfw=", - "x86_64-darwin": "sha256-V4AQH868dOfVhEj1mKTnZlhpZwFqBYsHS28Tx9VXHkE=" + "x86_64-linux": "sha256-zjYaBa+TKuHVBO0z0YqOOFLOxpxj2hB6Iq1o+BXBeVc=", + "aarch64-linux": "sha256-S2QDDoKcg8QCqjxCnMmLp/3xmiVHAbCJ0CuHQRjDBbE=", + "aarch64-darwin": "sha256-GtdfHbk4fhvosgo6iEjXrWSxteK+eNrT0P9swEccbew=", + "x86_64-darwin": "sha256-zmWFdPSTS/DpU40CgHA/KKiSIs0PlI092TzLv/cG0jE=" } } diff --git a/packages/app/e2e/performance/timeline/timeline-test-helpers.ts b/packages/app/e2e/performance/timeline/timeline-test-helpers.ts index 401fb74496c..6360b4a7931 100644 --- a/packages/app/e2e/performance/timeline/timeline-test-helpers.ts +++ b/packages/app/e2e/performance/timeline/timeline-test-helpers.ts @@ -45,7 +45,7 @@ export async function installStressSessionTabs(page: Page, input?: { draftID?: s }), ) localStorage.setItem( - "opencode.global.dat:tabs", + "opencode.window.browser.dat:tabs", JSON.stringify([ ...sessionIDs.map((sessionId) => ({ type: "session", diff --git a/packages/app/e2e/regression/cross-server-tab-close.spec.ts b/packages/app/e2e/regression/cross-server-tab-close.spec.ts index 031440f1660..159b5a50676 100644 --- a/packages/app/e2e/regression/cross-server-tab-close.spec.ts +++ b/packages/app/e2e/regression/cross-server-tab-close.spec.ts @@ -14,7 +14,7 @@ test("closing the active server's last tab opens the remaining server tab", asyn localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } })) localStorage.setItem("opencode.global.dat:server", JSON.stringify({ list: [serverB] })) localStorage.setItem( - "opencode.global.dat:tabs", + "opencode.window.browser.dat:tabs", JSON.stringify([ { type: "session", server: "http://127.0.0.1:4096", sessionId: sessionA }, { type: "session", server: serverB, sessionId: sessionB }, @@ -52,7 +52,7 @@ test("legacy session routes preserve an existing tab's server", async ({ page }) localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } })) localStorage.setItem("opencode.global.dat:server", JSON.stringify({ list: [serverB] })) localStorage.setItem( - "opencode.global.dat:tabs", + "opencode.window.browser.dat:tabs", JSON.stringify([{ type: "session", server: serverB, sessionId: sessionB }]), ) }, diff --git a/packages/app/e2e/regression/prompt-thinking-level.spec.ts b/packages/app/e2e/regression/prompt-thinking-level.spec.ts index d12684c143f..4219699f28c 100644 --- a/packages/app/e2e/regression/prompt-thinking-level.spec.ts +++ b/packages/app/e2e/regression/prompt-thinking-level.spec.ts @@ -66,7 +66,7 @@ test("shows the V2 thinking level control while relevant", async ({ page }) => { await expect(control).toBeVisible() await control.locator('[data-action="prompt-model-variant"]').click() - const high = page.getByRole("option", { name: "high" }) + const high = page.getByRole("menuitemradio", { name: "high" }) await expect(high).toBeVisible() await page.mouse.move(0, 0) await expect(control).toBeVisible() diff --git a/packages/app/e2e/regression/session-request-docks.spec.ts b/packages/app/e2e/regression/session-request-docks.spec.ts index e66cf81504e..036eaaef422 100644 --- a/packages/app/e2e/regression/session-request-docks.spec.ts +++ b/packages/app/e2e/regression/session-request-docks.spec.ts @@ -38,6 +38,29 @@ test("shows a pending question dock", async ({ page }) => { await expect(question.getByRole("radio", { name: /Extended/ })).toBeVisible() await expect(page.locator('[data-component="session-composer"]')).toHaveCount(0) + const rejectRequests: string[] = [] + page.on("request", (request) => { + if (request.method() !== "POST") return + if (new URL(request.url()).pathname === "/question/question-request/reject") rejectRequests.push(request.url()) + }) + + await question.locator('[data-component="icon-button"][data-icon="chevron-down"]').click() + await expect(question).toBeVisible() + await expect(question.getByText("Which implementation should be used?")).toBeVisible() + await expect(question.getByText("Select one answer")).toBeHidden() + await expect(question.getByRole("radio", { name: /Minimal/ })).toBeHidden() + await expect(question.getByRole("radio", { name: /Extended/ })).toBeHidden() + await expect(question.getByRole("button", { name: "Dismiss" })).toBeVisible() + await expect(question.getByRole("button", { name: "Submit" })).toBeVisible() + await expect(page.locator('[data-component="question-minimized-dock"]')).toHaveCount(0) + expect(rejectRequests).toEqual([]) + + await question.locator('[data-component="icon-button"][data-icon="chevron-down"]').click() + await expect(question).toBeVisible() + await expect(question.getByText("Which implementation should be used?")).toBeVisible() + await expect(question.getByRole("radio", { name: /Minimal/ })).toBeVisible() + expect(rejectRequests).toEqual([]) + await question.getByRole("radio", { name: /Minimal/ }).click() const reply = page.waitForRequest( (request) => request.method() === "POST" && new URL(request.url()).pathname === "/question/question-request/reply", diff --git a/packages/app/e2e/regression/session-todo-dock-navigation.spec.ts b/packages/app/e2e/regression/session-todo-dock-navigation.spec.ts index fd0b00f71cd..603c411d551 100644 --- a/packages/app/e2e/regression/session-todo-dock-navigation.spec.ts +++ b/packages/app/e2e/regression/session-todo-dock-navigation.spec.ts @@ -145,7 +145,7 @@ async function configurePage(page: Page) { }), ) localStorage.setItem( - "opencode.global.dat:tabs", + "opencode.window.browser.dat:tabs", JSON.stringify(sessionIDs.map((sessionId) => ({ type: "session", server, dirBase64, sessionId }))), ) }, diff --git a/packages/app/e2e/smoke/session-timeline.spec.ts b/packages/app/e2e/smoke/session-timeline.spec.ts index 5bca533e6a6..a73cc0ccdd7 100644 --- a/packages/app/e2e/smoke/session-timeline.spec.ts +++ b/packages/app/e2e/smoke/session-timeline.spec.ts @@ -127,7 +127,7 @@ test.describe("smoke: session timeline", () => { await page.addInitScript( ({ dirBase64, sourceID, targetID }) => { localStorage.setItem( - "opencode.global.dat:tabs", + "opencode.window.browser.dat:tabs", JSON.stringify( [sourceID, targetID].map((sessionId) => ({ type: "session", @@ -253,7 +253,7 @@ test.describe("smoke: session timeline", () => { await page.addInitScript( ({ dirBase64, sourceID, targetID }) => { localStorage.setItem( - "opencode.global.dat:tabs", + "opencode.window.browser.dat:tabs", JSON.stringify( [sourceID, targetID].map((sessionId) => ({ type: "session", diff --git a/packages/app/package.json b/packages/app/package.json index 52420afedc1..5749e754a85 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.17.11", + "version": "1.17.13", "description": "", "type": "module", "exports": { diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index 5b9223b5eeb..4ab0a95c23e 100644 --- a/packages/app/src/app.tsx +++ b/packages/app/src/app.tsx @@ -28,22 +28,22 @@ import { Show, } from "solid-js" import { Dynamic } from "solid-js/web" -import { CommandProvider } from "@/context/command" +import { CommandProvider, useCommand, type CommandOption } from "@/context/command" import { CommentsProvider } from "@/context/comments" import { FileProvider } from "@/context/file" -import { ServerSDKProvider, useServerSDK } from "@/context/server-sdk" -import { ServerSyncProvider, useServerSync } from "@/context/server-sync" +import { ServerSDKProvider } from "@/context/server-sdk" +import { ServerSyncProvider } from "@/context/server-sync" import { GlobalProvider, useGlobal } from "@/context/global" import { HighlightsProvider } from "@/context/highlights" import { LanguageProvider, type Locale, useLanguage } from "@/context/language" import { LayoutProvider } from "@/context/layout" import { ModelsProvider } from "@/context/models" -import { NotificationProvider, useNotification } from "@/context/notification" +import { NotificationProvider } from "@/context/notification" import { PermissionProvider } from "@/context/permission" +import { usePlatform } from "@/context/platform" import { PromptProvider } from "@/context/prompt" import { ServerConnection, ServerProvider, serverName, useServer } from "@/context/server" import { SettingsProvider, useSettings } from "@/context/settings" -import { TerminalProvider } from "@/context/terminal" import { TabsProvider, useTabs, type DraftTab } from "@/context/tabs" import { SDKProvider, useSDK } from "@/context/sdk" import { WslServersProvider } from "@/wsl/context" @@ -52,16 +52,9 @@ import LegacyLayout from "@/pages/layout" import NewLayout from "@/pages/layout-new" import { ErrorPage } from "./pages/error" import { useCheckServerHealth } from "./utils/server-health" -import { - legacySessionHref, - legacySessionServer, - requireServerKey, - selectSessionLineage, - sessionHref, -} from "./utils/session-route" -import { isSessionNotFoundError } from "./utils/server-errors" +import { legacySessionServer, requireServerKey, sessionHref } from "./utils/session-route" -import Session from "@/pages/session" +import { SessionPage, TargetSessionRoute as TargetSessionRouteContent } from "@/pages/session" import { NewHome, LegacyHome } from "@/pages/home" const NewSession = lazy(() => import("@/pages/new-session")) @@ -95,11 +88,7 @@ const SessionRoute = () => { tabs.newDraft({ server: server.key, directory: sdk().directory }, search.prompt) }) - return ( - - - - ) + return } const TargetSessionRoute = () => { @@ -114,76 +103,13 @@ const TargetSessionRoute = () => { - + ) } -function ResolvedTargetSessionRoute() { - const params = useParams<{ serverKey: string; id: string }>() - const settings = useSettings() - const tabs = useTabs() - const sync = useServerSync() - const serverKey = createMemo(() => requireServerKey(params.serverKey)) - const cached = createMemo(() => sync().session.lineage.peek(params.id)) - const [resolved] = createResource( - () => { - if (cached()) return - return { id: params.id, server: serverKey(), sync: sync() } - }, - ({ id, server, sync }) => - sync.session.lineage.resolve(id).catch((error) => { - if (isSessionNotFoundError(error, id)) tabs.removeSessionTab({ server, sessionId: id }) - throw error - }), - ) - const current = createMemo(() => selectSessionLineage(params.id, cached(), resolved())) - const directory = createMemo(() => current()?.session.directory) - const targetDirectory = () => directory()! - - createEffect(() => { - const session = current() - if (!session) return - tabs.addSessionTab({ - server: serverKey(), - sessionId: session.root.id, - }) - }) - - return ( - params.id}> - }> - - } - > - - - - - - - - - - ) -} - -function TargetSessionPage() { - const sdk = useSDK() - const serverSDK = useServerSDK() - return ( - - - - - - ) -} - // Wraps the non-draft routes. They are gated on (and keyed to) the globally selected // server via ServerKey, then provide the server-scoped shell (Permission/Layout/ // Notification/Models + the visual Layout) for that server. @@ -231,7 +157,7 @@ function ResolvedDraftRoute(props: { draft: DraftTab }) { - + @@ -239,7 +165,7 @@ function ResolvedDraftRoute(props: { draft: DraftTab }) { - + @@ -300,12 +226,36 @@ function SharedProviders(props: ParentProps) { <> + {props.children} ) } +function DesktopCommands() { + const command = useCommand() + const language = useLanguage() + const platform = usePlatform() + + command.register("desktop", () => { + const commands: CommandOption[] = [] + if (platform.platform === "desktop" && platform.exportDebugLogs) { + commands.push({ + id: "logs.export", + title: "Export logs", + category: language.t("command.category.settings"), + onSelect: () => { + void platform.exportDebugLogs?.() + }, + }) + } + return commands + }) + + return null +} + // Server-scoped providers shared by the legacy shell and the top-level new shell. type ServerScopedShellProps = ParentProps<{ directory?: () => string | undefined @@ -340,38 +290,14 @@ function NewAppLayout(props: ParentProps) { ) } -function TargetServerScopedProviders(props: ServerScopedShellProps) { +function DraftServerScopedProviders(props: ParentProps<{ directory?: () => string | undefined }>) { return ( - {props.children} ) } -function MarkSessionNotificationsViewed(props: { sessionID?: () => string | undefined }) { - const notification = useNotification() - createEffect(() => { - const sessionID = props.sessionID?.() - if (!notification.ready() || !sessionID) return - if (notification.session.unseenCount(sessionID) === 0) return - notification.session.markViewed(sessionID) - }) - return null -} - -function SessionProviders(props: ParentProps) { - return ( - - - - {props.children} - - - - ) -} - // The draft page only renders the prompt composer, so it drops TerminalProvider. // FileProvider and CommentsProvider stay because PromptInput uses file search and comment context. function DraftProviders(props: ParentProps) { diff --git a/packages/app/src/components/dialog-manage-models.tsx b/packages/app/src/components/dialog-manage-models.tsx index a0821ef1a36..0238f543c41 100644 --- a/packages/app/src/components/dialog-manage-models.tsx +++ b/packages/app/src/components/dialog-manage-models.tsx @@ -3,13 +3,26 @@ import { List } from "@opencode-ai/ui/list" import { Switch } from "@opencode-ai/ui/switch" import { Tooltip } from "@opencode-ai/ui/tooltip" import { Button } from "@opencode-ai/ui/button" -import type { Component } from "solid-js" +import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2" +import { Dialog as DialogV2, DialogBody, DialogHeader, DialogTitleGroup } from "@opencode-ai/ui/v2/dialog-v2" +import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon" +import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2" +import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2" +import { Switch as SwitchV2 } from "@opencode-ai/ui/v2/switch-v2" +import { ProviderIcon } from "@opencode-ai/ui/provider-icon" +import { useFilteredList } from "@opencode-ai/ui/hooks" +import { For, Show, type Component } from "solid-js" import { useLocal } from "@/context/local" import { popularProviders } from "@/hooks/use-providers" import { useLanguage } from "@/context/language" import { useDialog } from "@opencode-ai/ui/context/dialog" import { DialogSelectProvider } from "./dialog-select-provider" import { decode64 } from "@/utils/base64" +import { SettingsListV2 } from "./settings-v2/parts/list" +import { SettingsRowV2 } from "./settings-v2/parts/row" +import "./settings-v2/settings-v2.css" + +type ModelItem = ReturnType["model"]["list"]>[number] export const DialogManageModels: Component = () => { const local = useLocal() @@ -102,3 +115,151 @@ export const DialogManageModels: Component = () => { ) } + +export const DialogManageModelsV2: Component = () => { + const local = useLocal() + const language = useLanguage() + const dialog = useDialog() + const directory = () => decode64(local.slug()) + + const handleConnectProvider = () => { + dialog.show(() => ) + } + const providerList = (providerID: string) => local.model.list().filter((x) => x.provider.id === providerID) + const providerVisible = (providerID: string) => + providerList(providerID).every((x) => local.model.visible({ modelID: x.id, providerID: x.provider.id })) + const setProviderVisibility = (providerID: string, checked: boolean) => { + providerList(providerID).forEach((x) => { + local.model.setVisibility({ modelID: x.id, providerID: x.provider.id }, checked) + }) + } + const setModelVisibility = (item: ModelItem, checked: boolean) => { + local.model.setVisibility({ modelID: item.id, providerID: item.provider.id }, checked) + } + const list = useFilteredList({ + items: () => local.model.list(), + key: (x) => `${x.provider.id}:${x.id}`, + filterKeys: ["provider.name", "name", "id"], + sortBy: (a, b) => a.name.localeCompare(b.name), + groupBy: (x) => x.provider.id, + sortGroupsBy: (a, b) => { + const aRank = popularProviders.indexOf(a.category) + const bRank = popularProviders.indexOf(b.category) + const aPopular = aRank >= 0 + const bPopular = bRank >= 0 + if (aPopular && !bPopular) return -1 + if (!aPopular && bPopular) return 1 + return aRank - bRank + }, + }) + + return ( + + + + + {language.t("command.provider.connect")} + + + +
+
+ list.onInput(event.currentTarget.value)} + placeholder={language.t("dialog.model.search.placeholder")} + spellcheck={false} + autocorrect="off" + autocomplete="off" + autocapitalize="off" + autofocus + aria-label={language.t("dialog.model.search.placeholder")} + /> + + } + onClick={() => list.clear()} + aria-label={language.t("common.clear")} + /> + +
+
+
+
+ + {language.t("common.loading")} + {language.t("common.loading.ellipsis")} +
+ } + > + 0} + fallback={ +
+ {language.t("dialog.model.empty")} + + "{list.filter()}" + +
+ } + > + + {(group) => ( +
+
+
+ +

{group.items[0].provider.name}

+
+
+ setProviderVisibility(group.category, checked)} + hideLabel + > + {group.items[0].provider.name} + +
+
+ + + {(item) => ( + +
+ setModelVisibility(item, checked)} + hideLabel + > + {item.name} + +
+
+ )} +
+
+
+ )} +
+
+ +
+ +
+
+ ) +} diff --git a/packages/app/src/components/dialog-select-model.tsx b/packages/app/src/components/dialog-select-model.tsx index b4e8b3b719c..f3dd3ce20d1 100644 --- a/packages/app/src/components/dialog-select-model.tsx +++ b/packages/app/src/components/dialog-select-model.tsx @@ -1,15 +1,19 @@ import { Popover as Kobalte } from "@kobalte/core/popover" -import { Component, ComponentProps, createMemo, JSX, Show, ValidComponent } from "solid-js" +import { Component, ComponentProps, createMemo, For, JSX, Show, ValidComponent } from "solid-js" import { createStore } from "solid-js/store" import { useLocal } from "@/context/local" import { useDialog } from "@opencode-ai/ui/context/dialog" import { popularProviders } from "@/hooks/use-providers" import { Button } from "@opencode-ai/ui/button" import { IconButton } from "@opencode-ai/ui/icon-button" +import { ScrollView } from "@opencode-ai/ui/scroll-view" import { Tag } from "@opencode-ai/ui/tag" import { Dialog } from "@opencode-ai/ui/dialog" import { List } from "@opencode-ai/ui/list" import { Tooltip } from "@opencode-ai/ui/tooltip" +import { Icon } from "@opencode-ai/ui/v2/icon" +import { Tag as TagV2 } from "@opencode-ai/ui/v2/badge-v2" +import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2" import { ModelTooltip } from "./model-tooltip" import { useLanguage } from "@/context/language" import { decode64 } from "@/utils/base64" @@ -18,6 +22,22 @@ const isFree = (provider: string, cost: { input: number } | undefined) => provider === "opencode" && (!cost || cost.input === 0) type ModelState = ReturnType["model"] +type ModelItem = ReturnType[number] + +const modelKey = (model: ModelItem) => `${model.provider.id}:${model.id}` +const manageKey = "action:manage" + +const sortModelGroups = (a: { category: string; items: ModelItem[] }, b: { category: string; items: ModelItem[] }) => { + const aIndex = popularProviders.indexOf(a.category) + const bIndex = popularProviders.indexOf(b.category) + const aPopular = aIndex >= 0 + const bPopular = bIndex >= 0 + + if (aPopular && !bPopular) return -1 + if (!aPopular && bPopular) return 1 + if (aPopular && bPopular) return aIndex - bIndex + return a.items[0].provider.name.localeCompare(b.items[0].provider.name) +} const ModelList: Component<{ provider?: string @@ -200,6 +220,271 @@ export function ModelSelectorPopover(props: { ) } +export function ModelSelectorPopoverV2(props: { + provider?: string + model?: ModelState + children?: JSX.Element + triggerAs?: ValidComponent + triggerProps?: ModelSelectorTriggerProps + onClose?: () => void +}) { + const model = props.model ?? useLocal().model + const language = useLanguage() + const dialog = useDialog() + const [store, setStore] = createStore({ open: false, search: "", active: "" }) + let searchRef: HTMLInputElement | undefined + let contentRef: HTMLDivElement | undefined + let restoreTrigger = true + + const allModels = createMemo(() => + model + .list() + .filter((item) => model.visible({ modelID: item.id, providerID: item.provider.id })) + .filter((item) => (props.provider ? item.provider.id === props.provider : true)), + ) + const models = createMemo(() => { + const search = store.search.trim().toLowerCase() + const filtered = search + ? allModels().filter( + (item) => + item.name.toLowerCase().includes(search) || + item.id.toLowerCase().includes(search) || + item.provider.name.toLowerCase().includes(search), + ) + : allModels() + + return [...filtered].sort((a, b) => a.name.localeCompare(b.name)) + }) + const groups = createMemo(() => { + const byProvider = new Map() + for (const item of models()) { + byProvider.set(item.provider.id, [...(byProvider.get(item.provider.id) ?? []), item]) + } + return Array.from(byProvider, ([category, items]) => ({ category, items })).sort(sortModelGroups) + }) + const keys = () => [...models().map(modelKey), manageKey] + const current = () => { + const value = model.current() + return value ? `${value.provider.id}:${value.id}` : undefined + } + const initialActive = () => { + const selected = current() + const options = keys() + if (selected && options.includes(selected)) return selected + return options[0] ?? "" + } + const activeItem = () => + store.active ? contentRef?.querySelector(`[data-option-key="${CSS.escape(store.active)}"]`) : undefined + const afterClose = (callback: () => void) => { + const complete = () => { + if (contentRef?.isConnected) { + requestAnimationFrame(complete) + return + } + requestAnimationFrame(() => requestAnimationFrame(callback)) + } + requestAnimationFrame(complete) + } + const setOpen = (open: boolean) => { + if (open) { + restoreTrigger = true + setStore({ open: true, active: initialActive() }) + setTimeout(() => + requestAnimationFrame(() => { + searchRef?.focus() + activeItem()?.scrollIntoView({ block: "nearest" }) + }), + ) + return + } + setStore({ open: false, search: "", active: "" }) + } + const select = (item: ModelItem) => { + model.set({ modelID: item.id, providerID: item.provider.id }, { recent: true }) + props.onClose?.() + } + const selectModel = (item: ModelItem) => { + restoreTrigger = false + setOpen(false) + afterClose(() => select(item)) + } + const manage = () => { + restoreTrigger = false + setOpen(false) + afterClose(() => { + void import("./dialog-manage-models").then((x) => { + dialog.show(() => ) + }) + }) + } + const selectActive = () => { + const item = models().find((item) => modelKey(item) === store.active) + if (item) { + selectModel(item) + return + } + if (store.active === manageKey) manage() + } + const moveActive = (delta: number) => { + const options = keys() + if (options.length === 0) return + const index = options.indexOf(store.active) + const start = index === -1 ? 0 : index + setStore("active", options[(start + delta + options.length) % options.length]) + queueMicrotask(() => activeItem()?.scrollIntoView({ block: "nearest" })) + } + const setSearch = (value: string) => { + const search = value.trim().toLowerCase() + const first = [...allModels()] + .sort((a, b) => a.name.localeCompare(b.name)) + .find( + (item) => + !search || + item.name.toLowerCase().includes(search) || + item.id.toLowerCase().includes(search) || + item.provider.name.toLowerCase().includes(search), + ) + setStore({ search: value, active: first ? modelKey(first) : manageKey }) + } + + return ( + + + {props.children} + + + (contentRef = el)} + class="w-[284px] overflow-hidden rounded-md border-0 bg-v2-background-bg-layer-01 !p-0 shadow-[var(--v2-elevation-floating)] focus:outline-none" + onPointerDownOutside={() => (restoreTrigger = false)} + onFocusOutside={() => (restoreTrigger = false)} + onCloseAutoFocus={(event) => { + if (!restoreTrigger) event.preventDefault() + }} + > +
+
+ + (searchRef = el)} + value={store.search} + placeholder={language.t("dialog.model.search.placeholder")} + class="h-7 min-w-0 flex-1 border-0 bg-transparent text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-base outline-none placeholder:text-v2-text-text-faint" + spellcheck={false} + autocorrect="off" + autocomplete="off" + autocapitalize="off" + onInput={(event) => setSearch(event.currentTarget.value)} + onKeyDown={(event) => { + if (event.key === "Tab") return + event.stopPropagation() + if (event.key === "Escape") { + event.preventDefault() + restoreTrigger = false + setOpen(false) + afterClose(() => props.onClose?.()) + return + } + if (event.altKey || event.metaKey) return + if (event.key === "ArrowDown") { + event.preventDefault() + moveActive(1) + return + } + if (event.key === "ArrowUp") { + event.preventDefault() + moveActive(-1) + return + } + if (event.key === "Enter" && !event.isComposing) { + event.preventDefault() + selectActive() + } + }} + /> + + + +
+
+
+ +
+ 0} + fallback={ +
+ {language.t("dialog.model.empty")} +
+ } + > + + {(group) => ( + + + {group.items[0].provider.name} + + + + {(item) => ( + { + setStore("active", modelKey(item)) + setTimeout(() => searchRef?.focus()) + }} + onSelect={() => selectModel(item)} + > + {item.name} + + {language.t("model.tag.free")} + + + {language.t("model.tag.latest")} + + + )} + + + + )} + +
+
+
+
+
+ { + setStore("active", manageKey) + setTimeout(() => searchRef?.focus()) + }} + onSelect={manage} + > + + {language.t("dialog.model.manage")} + +
+ + + + ) +} + export const DialogSelectModel: Component<{ provider?: string; model?: ModelState }> = (props) => { const dialog = useDialog() const language = useLanguage() diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index e6f933f0f76..c72df7e5123 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -35,12 +35,14 @@ import { DockShellForm, DockTray } from "@opencode-ai/ui/dock-surface" import { Icon } from "@opencode-ai/ui/icon" import { ProviderIcon } from "@opencode-ai/ui/provider-icon" import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip" +import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2" import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2" +import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2" import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2" import { IconButton } from "@opencode-ai/ui/icon-button" import { Select } from "@opencode-ai/ui/select" import { useDialog } from "@opencode-ai/ui/context/dialog" -import { ModelSelectorPopover } from "@/components/dialog-select-model" +import { ModelSelectorPopover, ModelSelectorPopoverV2 } from "@/components/dialog-select-model" import { useCommand } from "@/context/command" import { Persist, persisted } from "@/utils/persist" import { usePermission } from "@/context/permission" @@ -68,6 +70,7 @@ import { promptPlaceholder } from "./prompt-input/placeholder" import { createPromptInputTransientState } from "./prompt-input/transient-state" import { showToast } from "@/utils/toast" import { ImagePreview } from "@opencode-ai/ui/image-preview" +import type { ReferenceInfo } from "@opencode-ai/sdk/v2/client" export type PromptInputState = ReturnType @@ -214,6 +217,7 @@ export const PromptInput: Component = (props) => { let fileInputRef: HTMLInputElement | undefined let scrollRef!: HTMLDivElement let slashPopoverRef!: HTMLDivElement + let restoreEndOnFocus = true const mirror = { input: false } const inset = 56 @@ -593,6 +597,16 @@ export const PromptInput: Component = (props) => { }) } + const handleFocus = () => { + if (!restoreEndOnFocus) return + restoreEndOnFocus = false + requestAnimationFrame(() => { + if (document.activeElement !== editorRef) return + setCursorPosition(editorRef, prompt.cursor() ?? promptLength(prompt.current())) + queueScroll() + }) + } + const renderEditorWithCursor = (parts: Prompt) => { const cursor = currentCursor() renderEditor(parts) @@ -629,24 +643,89 @@ export const PromptInput: Component = (props) => { }) } + const referenceDescription = (reference: ReferenceInfo) => + reference.source.type === "git" ? reference.source.repository : reference.source.path + + const referenceList = createMemo(() => + sync() + .data.reference.filter((reference) => !reference.hidden) + .map( + (reference): AtOption => ({ + type: "reference", + name: reference.name, + path: reference.path, + display: reference.name, + description: reference.description ?? referenceDescription(reference), + }), + ), + ) + const agentList = createMemo(() => props.controls.agents.available .filter((agent) => !agent.hidden && agent.mode !== "primary") .map((agent): AtOption => ({ type: "agent", name: agent.name, display: agent.name })), ) + const mcpResourceList = createMemo(() => + Object.values(sync().data.mcp_resource).map( + (resource): AtOption => ({ + type: "resource", + name: resource.name, + uri: resource.uri, + client: resource.client, + display: resource.name, + description: resource.description, + mime: resource.mimeType, + }), + ), + ) + const handleAtSelect = (option: AtOption | undefined) => { if (!option) return if (option.type === "agent") { addPart({ type: "agent", name: option.name, content: "@" + option.name, start: 0, end: 0 }) - } else { - addPart({ type: "file", path: option.path, content: "@" + option.path, start: 0, end: 0 }) + return } + if (option.type === "reference") { + addPart({ + type: "file", + path: option.path, + content: "@" + option.name, + start: 0, + end: 0, + mime: "application/x-directory", + filename: option.name, + }) + return + } + if (option.type === "resource") { + addPart({ + type: "file", + path: option.uri, + content: "@" + option.name, + start: 0, + end: 0, + mime: option.mime ?? "text/plain", + filename: option.name, + url: option.uri, + source: { + type: "resource", + text: { value: "@" + option.name, start: 0, end: 0 }, + clientName: option.client, + uri: option.uri, + }, + }) + return + } + addPart({ type: "file", path: option.path, content: "@" + option.path, start: 0, end: 0 }) } const atKey = (x: AtOption | undefined) => { if (!x) return "" - return x.type === "agent" ? `agent:${x.name}` : `file:${x.path}` + if (x.type === "agent") return `agent:${x.name}` + if (x.type === "reference") return `reference:${x.name}` + if (x.type === "resource") return `resource:${x.client}:${x.uri}` + return `file:${x.path}` } const { @@ -657,30 +736,36 @@ export const PromptInput: Component = (props) => { onKeyDown: atOnKeyDown, } = useFilteredList({ items: async (query) => { + const references = referenceList() const agents = agentList() + const mcpResources = mcpResourceList() const open = recent() const seen = new Set(open) const pinned: AtOption[] = open.map((path) => ({ type: "file", path, display: path, recent: true })) - if (!query.trim()) return [...agents, ...pinned] + if (!query.trim()) return [...references, ...agents, ...mcpResources, ...pinned] const paths = await files.searchFilesAndDirectories(query) const fileOptions: AtOption[] = paths .filter((path) => !seen.has(path)) .map((path) => ({ type: "file", path, display: path })) - return [...agents, ...pinned, ...fileOptions] + return [...references, ...agents, ...mcpResources, ...pinned, ...fileOptions] }, key: atKey, filterKeys: ["display"], skipFilter: (item) => item.type === "file" && !item.recent, groupBy: (item) => { + if (item.type === "reference") return "reference" if (item.type === "agent") return "agent" + if (item.type === "resource") return "resource" if (item.recent) return "recent" return "file" }, sortGroupsBy: (a, b) => { const rank = (category: string) => { - if (category === "agent") return 0 - if (category === "recent") return 1 - return 2 + if (category === "reference") return 0 + if (category === "agent") return 1 + if (category === "resource") return 2 + if (category === "recent") return 3 + return 4 } return rank(a.category) - rank(b.category) }, @@ -746,7 +831,17 @@ export const PromptInput: Component = (props) => { const pill = document.createElement("span") pill.textContent = part.content pill.setAttribute("data-type", part.type) - if (part.type === "file") pill.setAttribute("data-path", part.path) + if (part.type === "file") { + pill.setAttribute("data-path", part.path) + if (part.mime) pill.setAttribute("data-mime", part.mime) + if (part.filename) pill.setAttribute("data-filename", part.filename) + if (part.url) pill.setAttribute("data-url", part.url) + if (part.source?.type === "resource") { + pill.setAttribute("data-source-type", part.source.type) + pill.setAttribute("data-source-client-name", part.source.clientName) + pill.setAttribute("data-source-uri", part.source.uri) + } + } if (part.type === "agent") pill.setAttribute("data-name", part.name) pill.setAttribute("contenteditable", "false") pill.style.userSelect = "text" @@ -791,8 +886,7 @@ export const PromptInput: Component = (props) => { } } - // Auto-scroll active command into view when navigating with keyboard - createEffect(() => { + const scrollSlashActiveIntoView = () => { const activeId = slashActive() if (!activeId || !slashPopoverRef) return @@ -800,7 +894,7 @@ export const PromptInput: Component = (props) => { const element = slashPopoverRef.querySelector(`[data-slash-id="${activeId}"]`) element?.scrollIntoView({ block: "nearest", behavior: "smooth" }) }) - }) + } const selectPopoverActive = () => { if (store.popover === "at") { const items = atFlat() @@ -862,12 +956,29 @@ export const PromptInput: Component = (props) => { const pushFile = (file: HTMLElement) => { const content = file.textContent ?? "" + const source = + file.dataset.sourceType === "resource" && file.dataset.sourceClientName && file.dataset.sourceUri + ? { + type: "resource" as const, + text: { + value: content, + start: position, + end: position + content.length, + }, + clientName: file.dataset.sourceClientName, + uri: file.dataset.sourceUri, + } + : undefined parts.push({ type: "file", path: file.dataset.path!, content, start: position, end: position + content.length, + ...(file.dataset.mime ? { mime: file.dataset.mime } : {}), + ...(file.dataset.filename ? { filename: file.dataset.filename } : {}), + ...(file.dataset.url ? { url: file.dataset.url } : {}), + ...(source ? { source } : {}), }) position += content.length } @@ -1287,6 +1398,9 @@ export const PromptInput: Component = (props) => { } if (store.popover === "slash") { slashOnKeyDown(event) + if (event.key === "ArrowUp" || event.key === "ArrowDown" || ctrlNav) { + scrollSlashActiveIntoView() + } } event.preventDefault() return @@ -1368,6 +1482,7 @@ export const PromptInput: Component = (props) => { model: props.controls.model.selection, providerID: props.controls.model.selection.current()?.provider?.id, modelName: props.controls.model.selection.current()?.name ?? language.t("dialog.model.select.title"), + newLayoutDesigns: props.controls.newLayoutDesigns, style: control(), onClose: restoreFocus, onUnpaidClick: () => { @@ -1378,6 +1493,11 @@ export const PromptInput: Component = (props) => { })) const newSession = () => props.variant === "new-session" + const bindEditorRef = (el: HTMLDivElement) => { + editorRef = el + restoreEndOnFocus = true + props.ref?.(el) + } const showAgentControl = createMemo(() => props.controls.agents.visible && props.controls.agents.options.length > 0) const agentControlState = createMemo(() => ({ title: language.t("command.agent.cycle"), @@ -1406,6 +1526,8 @@ export const PromptInput: Component = (props) => { setSlashActive={setSlashActive} onSlashSelect={handleSlashSelect} commandKeybind={command.keybind} + commandKeybindParts={command.keybindParts} + newLayoutDesigns={props.controls.newLayoutDesigns} t={(key) => language.t(key as Parameters[0])} /> @@ -1459,10 +1581,7 @@ export const PromptInput: Component = (props) => {
(scrollRef = el)}>
{ - editorRef = el - props.ref?.(el) - }} + ref={bindEditorRef} role="textbox" aria-multiline="true" aria-label={designPlaceholder()} @@ -1477,6 +1596,7 @@ export const PromptInput: Component = (props) => { onPaste={handlePaste} onCompositionStart={handleCompositionStart} onCompositionEnd={handleCompositionEnd} + onFocus={handleFocus} onBlur={handleBlur} onKeyDown={handleKeyDown} classList={{ @@ -1497,7 +1617,7 @@ export const PromptInput: Component = (props) => {
-
+
{fileAttachmentInput()} = (props) => {
= (props) => { } > - props.onHighlight()} onClick={() => props.onSelect(props.record.session)} @@ -1119,6 +1259,7 @@ function HomeSessionSearchResultRow(props: { session={props.record.session} server={props.server} activeServer={props.activeServer} + revealProjectOnHover={!!showProjectName()} />
void }) { - const language = useLanguage() +function HomeSessionGroupHeader(props: { + title: string + titleOpacity: number + ref: ComponentProps<"div">["ref"] + elevated?: boolean +}) { return ( -
- - - {(onNewSession) => ( - - {language.t("command.session.new")} - - )} - +
+
) } @@ -1170,7 +1306,10 @@ function HomeSessionRow(props: { const showProjectName = () => props.showProjectName && props.record.projectName return ( -
+
+ +
- - - } - footer={ - <> - -
- 0}> - - - -
- - } - > -
- {question()?.question} -
- {language.t("ui.question.singleHint")}
}> -
{language.t("ui.question.multiHint")}
- -
- - {(opt, i) => ( - +
+ 0}> + + + +
+ + } + > +
+ {question()?.question} +
+ + {language.t("ui.question.singleHint")}
}> +
{language.t("ui.question.multiHint")}
+ + +
(optionsRef = el)} + data-slot="question-options" + aria-hidden={store.minimized || optionsOff() ? "true" : undefined} + classList={{ "pointer-events-none": hidden() > 0.1 }} + style={{ + "max-height": `${Math.max(0, store.optionsHeight * (1 - hidden()))}px`, + opacity: `${Math.max(0, Math.min(1, 1 - hidden()))}`, + visibility: optionsOff() ? "hidden" : "visible", + }} + > + + {(opt, i) => ( + - setStore("focus", options().length)} + onClick={customOpen} + > + + + {customLabel()} + {input() || customPlaceholder()} + + + } + > +
setStore("focus", options().length)} - onClick={customOpen} + onMouseDown={(e) => { + if (sending()) { + e.preventDefault() + return + } + if (e.target instanceof HTMLTextAreaElement) return + const input = e.currentTarget.querySelector('[data-slot="question-custom-input"]') + if (input instanceof HTMLTextAreaElement) input.focus() + }} + onSubmit={(e) => { + e.preventDefault() + commitCustom() + }} > {customLabel()} - {input() || customPlaceholder()} - - - } - > - { - if (sending()) { - e.preventDefault() - return - } - if (e.target instanceof HTMLTextAreaElement) return - const input = e.currentTarget.querySelector('[data-slot="question-custom-input"]') - if (input instanceof HTMLTextAreaElement) input.focus() - }} - onSubmit={(e) => { - e.preventDefault() - commitCustom() - }} - > - - - {customLabel()} -