From 8ab08a4c5b0d6bd6c0364cf2797f6406a328a104 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Salavisa?= Date: Thu, 13 Aug 2026 00:15:19 +0100 Subject: [PATCH] refactor: remove core Voice Mode in favour of the Voice plugin (#2576) Co-authored-by: Kandev Agent --- README.md | 2 +- .../internal/backendapp/boot_state_routes.go | 13 - apps/backend/internal/backendapp/helpers.go | 8 - apps/backend/internal/backendapp/main.go | 1 - apps/backend/internal/common/config/config.go | 19 - .../internal/user/controller/controller.go | 1 - apps/backend/internal/user/dto/dto.go | 3 - apps/backend/internal/user/models/models.go | 26 - apps/backend/internal/user/service/service.go | 65 -- .../internal/user/service/service_test.go | 111 ---- apps/backend/internal/user/store/sqlite.go | 56 -- .../voice/handlers/transcribe_handlers.go | 117 ---- .../handlers/transcribe_handlers_test.go | 157 ----- .../internal/voice/transcribe/service.go | 185 ------ .../internal/voice/transcribe/service_test.go | 222 ------- apps/pnpm-lock.yaml | 565 ---------------- .../settings/settings-tree-render.test.tsx | 17 +- .../settings/external-mcp-settings.tsx | 2 +- .../settings/keyboard-shortcuts-card.tsx | 2 +- .../settings/task-behavior-settings.tsx | 2 - .../settings/voice-mode-settings.test.tsx | 123 ---- .../settings/voice-mode-settings.tsx | 626 ------------------ .../task-create-dialog-form-body.test.tsx | 8 +- .../task-create-dialog-form-body.tsx | 8 +- .../task-create-dialog-selectors.test.tsx | 102 ++- .../task-create-dialog-selectors.tsx | 39 +- .../components/task-create-dialog-types.ts | 6 +- apps/web/components/task-create-dialog.tsx | 14 +- .../components/task/chat/chat-input-body.tsx | 7 - .../task/chat/chat-input-container.tsx | 38 -- .../chat/chat-input-toolbar-composer.test.tsx | 4 - .../task/chat/chat-input-toolbar-desktop.tsx | 12 - .../task/chat/chat-input-toolbar-mobile.tsx | 10 - .../task/chat/chat-input-toolbar.test.tsx | 4 - .../task/chat/chat-input-toolbar.tsx | 8 - .../task/chat/voice-input-button.test.tsx | 245 ------- .../task/chat/voice-input-button.tsx | 425 ------------ .../chat/voice-model-load-indicator.test.tsx | 63 -- .../task/chat/voice-model-load-indicator.tsx | 67 -- .../task/new-session-dialog.test.tsx | 20 +- .../components/task/new-session-dialog.tsx | 6 +- apps/web/e2e/helpers/api-client.ts | 2 - .../e2e/tests/chat/mobile-voice-mode.spec.ts | 103 --- .../e2e/tests/chat/toolbar-overflow.spec.ts | 2 +- .../e2e/tests/i18n/pseudo-coverage.spec.ts | 12 +- .../e2e/tests/settings/settings-index.spec.ts | 22 - .../e2e/tests/settings/utility-agents.spec.ts | 6 +- .../system/message-queue-settings.spec.ts | 4 +- apps/web/eslint.i18n.options.mjs | 48 +- apps/web/generated/licenses.json | 360 ---------- .../hooks/use-ensure-user-settings.test.ts | 8 - apps/web/hooks/use-voice-input.test.ts | 199 ------ apps/web/hooks/use-voice-input.ts | 493 -------------- apps/web/lib/api/domains/plugins-api.ts | 6 +- apps/web/lib/api/domains/voice-api.test.ts | 63 -- apps/web/lib/api/domains/voice-api.ts | 51 -- apps/web/lib/keyboard/constants.ts | 6 - .../lib/keyboard/shortcut-overrides.test.ts | 3 +- apps/web/lib/keyboard/shortcut-overrides.ts | 2 - .../lib/settings-discovery/catalog.test.ts | 19 +- .../settings-discovery/catalog/preferences.ts | 71 -- apps/web/lib/ssr/user-settings.test.ts | 76 --- apps/web/lib/ssr/user-settings.ts | 34 +- apps/web/lib/state/slices/settings/types.ts | 25 - apps/web/lib/types/http-user-settings.ts | 3 - apps/web/lib/types/http-voice.ts | 17 - apps/web/lib/voice/capabilities.test.ts | 97 --- apps/web/lib/voice/capabilities.ts | 75 --- apps/web/lib/voice/whisper-web-client.ts | 199 ------ apps/web/lib/voice/whisper-web-models.ts | 42 -- apps/web/package.json | 1 - apps/web/src/locales/en/settings.json | 55 -- apps/web/src/locales/en/task.json | 15 - apps/web/src/locales/pseudo/settings.json | 55 -- apps/web/src/locales/pseudo/task.json | 15 - apps/web/src/locales/pt-pt/settings.json | 55 -- apps/web/src/locales/pt-pt/task.json | 15 - apps/web/src/locales/zh-cn/settings.json | 55 -- apps/web/src/locales/zh-cn/task.json | 15 - apps/web/workers/whisper-web.worker.ts | 138 ---- docs/features.md | 2 +- docs/public/configuration.md | 10 +- docs/public/developer-tools.md | 47 +- docs/public/feature-status.md | 2 +- docs/public/index.md | 2 +- docs/specs/INDEX.md | 5 +- docs/specs/plugins/voice-extraction-host.md | 5 +- docs/specs/plugins/voice-extraction.md | 87 +++ docs/specs/ui/voice-mode-task-behavior.md | 7 +- 89 files changed, 261 insertions(+), 5782 deletions(-) delete mode 100644 apps/backend/internal/voice/handlers/transcribe_handlers.go delete mode 100644 apps/backend/internal/voice/handlers/transcribe_handlers_test.go delete mode 100644 apps/backend/internal/voice/transcribe/service.go delete mode 100644 apps/backend/internal/voice/transcribe/service_test.go delete mode 100644 apps/web/components/settings/voice-mode-settings.test.tsx delete mode 100644 apps/web/components/settings/voice-mode-settings.tsx delete mode 100644 apps/web/components/task/chat/voice-input-button.test.tsx delete mode 100644 apps/web/components/task/chat/voice-input-button.tsx delete mode 100644 apps/web/components/task/chat/voice-model-load-indicator.test.tsx delete mode 100644 apps/web/components/task/chat/voice-model-load-indicator.tsx delete mode 100644 apps/web/e2e/tests/chat/mobile-voice-mode.spec.ts delete mode 100644 apps/web/hooks/use-voice-input.test.ts delete mode 100644 apps/web/hooks/use-voice-input.ts delete mode 100644 apps/web/lib/api/domains/voice-api.test.ts delete mode 100644 apps/web/lib/api/domains/voice-api.ts delete mode 100644 apps/web/lib/types/http-voice.ts delete mode 100644 apps/web/lib/voice/capabilities.test.ts delete mode 100644 apps/web/lib/voice/capabilities.ts delete mode 100644 apps/web/lib/voice/whisper-web-client.ts delete mode 100644 apps/web/lib/voice/whisper-web-models.ts delete mode 100644 apps/web/workers/whisper-web.worker.ts create mode 100644 docs/specs/plugins/voice-extraction.md diff --git a/README.md b/README.md index 25afaf8d1a..9cea7bb037 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ Open source, multi-provider, no telemetry, not tied to any cloud. - **Workspace isolation** - Git worktrees prevent concurrent agents from conflicting - **Multi-repository tasks** - Span a single task across multiple repositories, with one worktree per repo, per-repo branches, per-repo PRs, and per-repo grouping in the Changes panel and review dialog - **Flexible runtimes** - Run agents as local processes, in isolated Docker containers, on remote servers via SSH, or in cloud executors like sprites.dev -- **Runtime settings** - Executor profiles, secrets, custom prompts, utility agents, voice mode, and resource metrics are configurable from Settings +- **Runtime settings** - Executor profiles, secrets, custom prompts, utility agents, and resource metrics are configurable from Settings - **Task-agent MCP** - Agents can create subtasks, target sibling repos, attach extra branches for multiple PRs, message other tasks, read conversations, and inspect related tasks - **External MCP** - Manage Kandev from outside coding agents over streamable HTTP or SSE, with copyable config snippets for popular agent CLIs - **Workflow portability** - Export and import workflows as portable YAML across workspaces or Kandev installs diff --git a/apps/backend/internal/backendapp/boot_state_routes.go b/apps/backend/internal/backendapp/boot_state_routes.go index f39197fc17..1a6ff5137c 100644 --- a/apps/backend/internal/backendapp/boot_state_routes.go +++ b/apps/backend/internal/backendapp/boot_state_routes.go @@ -517,7 +517,6 @@ func mapUserSettingsState(response userdto.UserSettingsResponse, workspaceID str }, "appStatusBarEnabled": settings.AppStatusBarEnabled, "appStatusBarOrder": mapAppStatusBarOrder(settings.AppStatusBarOrder), - "voiceMode": mapVoiceMode(settings.VoiceMode), "hiddenWorkflowStepIds": stringSliceMap(settings.KanbanHiddenStepIDs), "loaded": true, } @@ -683,18 +682,6 @@ func mapTaskCreateLastUsed(value usermodels.TaskCreateLastUsed) map[string]any { } } -// mapVoiceMode maps the voice mode settings to the boot shape with defaults. -func mapVoiceMode(value usermodels.VoiceModeSettings) map[string]any { - return map[string]any{ - "enabled": value.Enabled, - "engine": defaultString(value.Engine, "auto"), - "language": defaultString(value.Language, "auto"), - "mode": defaultString(value.Mode, "toggle"), - "autoSend": value.AutoSend, - "whisperWebModel": defaultString(value.WhisperWebModel, "base"), - } -} - // resolveActiveOfficeWorkspaceID returns the cookie workspace id when it is valid, otherwise the first workspace id. func resolveActiveOfficeWorkspaceID(workspaces []taskdto.WorkspaceDTO, cookieWorkspaceID string) string { for _, workspace := range workspaces { diff --git a/apps/backend/internal/backendapp/helpers.go b/apps/backend/internal/backendapp/helpers.go index dc77f6c161..78d7766ce0 100644 --- a/apps/backend/internal/backendapp/helpers.go +++ b/apps/backend/internal/backendapp/helpers.go @@ -89,8 +89,6 @@ import ( userhandlers "github.com/kandev/kandev/internal/user/handlers" utilitycontroller "github.com/kandev/kandev/internal/utility/controller" utilityhandlers "github.com/kandev/kandev/internal/utility/handlers" - voicehandlers "github.com/kandev/kandev/internal/voice/handlers" - "github.com/kandev/kandev/internal/voice/transcribe" "github.com/kandev/kandev/internal/webapp" webembedded "github.com/kandev/kandev/internal/webapp/embedded" workflowcontroller "github.com/kandev/kandev/internal/workflow/controller" @@ -559,7 +557,6 @@ type routeParams struct { devMode bool httpPort int features config.FeaturesConfig - voice config.VoiceConfig homeDir string interimSettingsInterlockToken string log *logger.Logger @@ -1118,11 +1115,6 @@ func registerSecondaryRoutes( utilityhandlers.RegisterRoutes(p.router, p.utilityCtrl, p.lifecycleMgr, p.hostUtilityMgr, p.services.User, p.log) p.log.Debug("Registered Utility Agents handlers (HTTP)") - // Voice transcription fallback. The route always mounts, but returns 503 - // when no API key is configured so the frontend can hide the path. - voicehandlers.RegisterRoutes(p.router, transcribe.New(p.voice.OpenAIAPIKey), p.log) - p.log.Debug("Registered Voice handlers (HTTP)") - agentcapabilities.RegisterRoutes(p.router, p.hostUtilityMgr, p.log) p.log.Debug("Registered Agent Capabilities handlers (HTTP)") diff --git a/apps/backend/internal/backendapp/main.go b/apps/backend/internal/backendapp/main.go index 0cb0439188..b7b698a6bd 100644 --- a/apps/backend/internal/backendapp/main.go +++ b/apps/backend/internal/backendapp/main.go @@ -1947,7 +1947,6 @@ func buildHTTPServer( devMode: cfg.Debug.DevMode || cfg.Debug.PprofEnabled, httpPort: resolvedHTTPPort(cfg), features: cfg.Features, - voice: cfg.Voice, homeDir: cfg.ResolvedHomeDir(), interimSettingsInterlockToken: interimSettingsInterlockToken, log: log, diff --git a/apps/backend/internal/common/config/config.go b/apps/backend/internal/common/config/config.go index 4e5d9cc270..7d9f0ca3df 100644 --- a/apps/backend/internal/common/config/config.go +++ b/apps/backend/internal/common/config/config.go @@ -43,7 +43,6 @@ type Config struct { RepoClone RepoCloneConfig `mapstructure:"repoClone"` Debug DebugConfig `mapstructure:"debug"` Office OfficeConfig `mapstructure:"office"` - Voice VoiceConfig `mapstructure:"voice"` Features FeaturesConfig `mapstructure:"features"` GitHubCredentialBroker GitHubCredentialBrokerConfig `mapstructure:"githubCredentialBroker"` } @@ -355,20 +354,6 @@ type OfficeConfig struct { JWTSigningKey string `mapstructure:"jwtSigningKey"` } -// VoiceConfig holds configuration for the chat voice-input transcription -// fallback. The primary voice-input engine runs entirely in the browser -// (Web Speech API); this server-side fallback is only used when the browser -// has no SpeechRecognition support (e.g. Firefox). -// -// When OpenAIAPIKey is empty the /api/v1/transcribe endpoint returns 503 -// and the frontend hides the fallback path, so the feature is safe to -// ship un-configured. -type VoiceConfig struct { - // OpenAIAPIKey is the API key used to call OpenAI's Whisper transcription - // endpoint. Set via KANDEV_VOICE_OPENAI_API_KEY. - OpenAIAPIKey string `mapstructure:"openAIApiKey"` -} - // FeaturesConfig is the typed wire/config shape for runtime feature flags. // Every new release toggle defaults to false so production binaries ship with // new work hidden until a deployment explicitly opts in (env var, e.g. @@ -561,9 +546,6 @@ func setDefaults(v *viper.Viper) { // Office defaults v.SetDefault("office.jwtSigningKey", "") - // Voice defaults - v.SetDefault("voice.openAIApiKey", "") - // Feature-flag defaults live in ./features.yaml (symlinked to // apps/backend/internal/features/features.yaml). LoadWithPath applies // them via features.ApplyDefaults after this function returns so the @@ -676,7 +658,6 @@ func LoadWithPath(configPath string) (*Config, error) { _ = v.BindEnv("events.namespace", "KANDEV_EVENTS_NAMESPACE") _ = v.BindEnv("debug.devMode", "KANDEV_DEBUG_DEV_MODE") _ = v.BindEnv("debug.pprofEnabled", "KANDEV_DEBUG_PPROF_ENABLED") - _ = v.BindEnv("voice.openAIApiKey", "KANDEV_VOICE_OPENAI_API_KEY") _ = v.BindEnv( "githubCredentialBroker.publicBaseUrl", "KANDEV_GITHUB_CREDENTIAL_BROKER_PUBLIC_BASE_URL", diff --git a/apps/backend/internal/user/controller/controller.go b/apps/backend/internal/user/controller/controller.go index f6254eafdb..bbb6152cfd 100644 --- a/apps/backend/internal/user/controller/controller.go +++ b/apps/backend/internal/user/controller/controller.go @@ -105,7 +105,6 @@ func (c *Controller) UpdateUserSettings(ctx context.Context, req dto.UpdateUserS SystemMetricsDisplay: systemMetricsDisplayPatch(req.SystemMetricsDisplay), AppStatusBarEnabled: req.AppStatusBarEnabled, AppStatusBarOrder: req.AppStatusBarOrder, - VoiceMode: req.VoiceMode, KanbanHiddenStepIDs: req.KanbanHiddenStepIDs, }) if err != nil { diff --git a/apps/backend/internal/user/dto/dto.go b/apps/backend/internal/user/dto/dto.go index f0c8e51a26..bf27b1b432 100644 --- a/apps/backend/internal/user/dto/dto.go +++ b/apps/backend/internal/user/dto/dto.go @@ -70,7 +70,6 @@ type UserSettingsDTO struct { SystemMetricsDisplay models.SystemMetricsDisplaySettings `json:"system_metrics_display"` AppStatusBarEnabled bool `json:"app_status_bar_enabled"` AppStatusBarOrder models.AppStatusBarOrder `json:"app_status_bar_order"` - VoiceMode models.VoiceModeSettings `json:"voice_mode"` KanbanHiddenStepIDs map[string][]string `json:"kanban_hidden_step_ids"` Revision int64 `json:"revision"` UpdatedAt string `json:"updated_at"` @@ -146,7 +145,6 @@ type UpdateUserSettingsRequest struct { SystemMetricsDisplay *SystemMetricsDisplaySettingsPatch `json:"system_metrics_display,omitempty"` AppStatusBarEnabled *bool `json:"app_status_bar_enabled,omitempty"` AppStatusBarOrder *models.AppStatusBarOrder `json:"app_status_bar_order,omitempty"` - VoiceMode *models.VoiceModeSettings `json:"voice_mode,omitempty"` KanbanHiddenStepIDs *map[string][]string `json:"kanban_hidden_step_ids,omitempty"` } @@ -296,7 +294,6 @@ func FromUserSettings(settings *models.UserSettings) UserSettingsDTO { SystemMetricsDisplay: settings.SystemMetricsDisplay, AppStatusBarEnabled: settings.AppStatusBarEnabled, AppStatusBarOrder: settings.AppStatusBarOrder, - VoiceMode: settings.VoiceMode, KanbanHiddenStepIDs: settings.KanbanHiddenStepIDs, Revision: settings.Revision, UpdatedAt: settings.UpdatedAt.Format(time.RFC3339), diff --git a/apps/backend/internal/user/models/models.go b/apps/backend/internal/user/models/models.go index b31500ae89..86404fd3a4 100644 --- a/apps/backend/internal/user/models/models.go +++ b/apps/backend/internal/user/models/models.go @@ -126,7 +126,6 @@ type UserSettings struct { SystemMetricsDisplay SystemMetricsDisplaySettings `json:"system_metrics_display"` AppStatusBarEnabled bool `json:"app_status_bar_enabled"` AppStatusBarOrder AppStatusBarOrder `json:"app_status_bar_order"` - VoiceMode VoiceModeSettings `json:"voice_mode"` KanbanHiddenStepIDs map[string][]string `json:"kanban_hidden_step_ids"` Revision int64 `json:"revision"` CreatedAt time.Time `json:"created_at"` @@ -143,31 +142,6 @@ type AppStatusBarOrder struct { RightItemIDs []string `json:"right_item_ids"` } -// VoiceModeSettings is the per-user configuration surface for the chat -// voice-input feature. Stored as a nested JSON object inside the `users.settings` -// blob — adding fields here does not require a schema migration. -type VoiceModeSettings struct { - // Enabled gates the whole feature. When false, the mic button is hidden - // entirely and no voice-related hooks run on the chat input. Defaults to - // true for new users; pre-existing user rows that have no `enabled` field - // in their stored JSON are also treated as enabled (see store layer). - Enabled bool `json:"enabled"` - // Engine is the user's preferred transcription engine. - // "auto" | "webSpeech" | "whisperWeb" | "whisperServer". Default "auto". - Engine string `json:"engine"` - // Language is the BCP-47 tag or "auto" to use the browser's language. - // Examples: "en-US", "pt-PT", "ja-JP". Default "auto". - Language string `json:"language"` - // Mode controls how the mic button is activated: "toggle" (click to start/stop) - // or "hold" (push-to-talk). Default "toggle". - Mode string `json:"mode"` - // AutoSend submits the chat message immediately after the transcript is inserted. - AutoSend bool `json:"auto_send"` - // WhisperWebModel selects the in-browser Whisper model when engine = whisperWeb. - // "tiny" | "base" | "small". Default "base". - WhisperWebModel string `json:"whisper_web_model"` -} - // SavedLayout represents a user-saved dockview layout configuration. type SavedLayout struct { ID string `json:"id"` diff --git a/apps/backend/internal/user/service/service.go b/apps/backend/internal/user/service/service.go index 3d12e20811..37d59061fa 100644 --- a/apps/backend/internal/user/service/service.go +++ b/apps/backend/internal/user/service/service.go @@ -90,7 +90,6 @@ type UpdateUserSettingsRequest struct { SystemMetricsDisplay *SystemMetricsDisplaySettingsPatch AppStatusBarEnabled *bool AppStatusBarOrder *models.AppStatusBarOrder - VoiceMode *models.VoiceModeSettings KanbanHiddenStepIDs *map[string][]string } @@ -195,9 +194,6 @@ func (s *Service) UpdateUserSettings(ctx context.Context, req *UpdateUserSetting if err := applyUserPreferenceBlobs(settings, req); err != nil { return nil, fmt.Errorf("%w: %s", ErrValidation, err.Error()) } - if err := applyVoiceMode(settings, req.VoiceMode); err != nil { - return nil, fmt.Errorf("%w: %s", ErrValidation, err.Error()) - } settings.UpdatedAt = time.Now().UTC() var taskCreatePatch *models.TaskCreateLastUsed if req.TaskCreateLastUsed != nil && !taskCreateLastUsedPatchEmpty(*req.TaskCreateLastUsed) { @@ -550,66 +546,6 @@ func applyChangesPanelLayout(settings *models.UserSettings, value *string) error return nil } -var ( - validVoiceEngines = map[string]struct{}{ - "auto": {}, - "webSpeech": {}, - "whisperWeb": {}, - "whisperServer": {}, - } - validVoiceModes = map[string]struct{}{ - "toggle": {}, - "hold": {}, - } - validWhisperWebModels = map[string]struct{}{ - "tiny": {}, - "base": {}, - "small": {}, - } -) - -// applyVoiceMode validates the inbound voice-mode settings and merges them -// onto the user record. Each sub-field is validated independently so a -// partial update (e.g. just `engine`) still works. -// -// `enabled` and `auto_send` are plain bools — every PATCH carries them. The -// settings UI always sends the full VoiceMode object so partial updates that -// would otherwise zero these are not a real concern. -func applyVoiceMode(settings *models.UserSettings, value *models.VoiceModeSettings) error { - if value == nil { - return nil - } - current := settings.VoiceMode - if current.Engine == "" { - current.Engine = "auto" - } - if value.Engine != "" { - if _, ok := validVoiceEngines[value.Engine]; !ok { - return errors.New("voice_mode.engine must be 'auto', 'webSpeech', 'whisperWeb', or 'whisperServer'") - } - current.Engine = value.Engine - } - if value.Language != "" { - current.Language = strings.TrimSpace(value.Language) - } - if value.Mode != "" { - if _, ok := validVoiceModes[value.Mode]; !ok { - return errors.New("voice_mode.mode must be 'toggle' or 'hold'") - } - current.Mode = value.Mode - } - if value.WhisperWebModel != "" { - if _, ok := validWhisperWebModels[value.WhisperWebModel]; !ok { - return errors.New("voice_mode.whisper_web_model must be 'tiny', 'base', or 'small'") - } - current.WhisperWebModel = value.WhisperWebModel - } - current.AutoSend = value.AutoSend - current.Enabled = value.Enabled - settings.VoiceMode = current - return nil -} - // applyChatSubmitKey validates and applies the chat_submit_key setting. func (s *Service) applyChatSubmitKey(settings *models.UserSettings, req *UpdateUserSettingsRequest) error { if req.ChatSubmitKey == nil { @@ -880,7 +816,6 @@ func (s *Service) publishUserSettingsEvent(ctx context.Context, settings *models "system_metrics_display": settings.SystemMetricsDisplay, "app_status_bar_enabled": settings.AppStatusBarEnabled, "app_status_bar_order": settings.AppStatusBarOrder, - "voice_mode": settings.VoiceMode, "kanban_hidden_step_ids": settings.KanbanHiddenStepIDs, "revision": settings.Revision, "updated_at": settings.UpdatedAt.Format(time.RFC3339), diff --git a/apps/backend/internal/user/service/service_test.go b/apps/backend/internal/user/service/service_test.go index ba54b97b03..e5dd56581e 100644 --- a/apps/backend/internal/user/service/service_test.go +++ b/apps/backend/internal/user/service/service_test.go @@ -1818,114 +1818,3 @@ func (b *recordingEventBus) Close() {} func (b *recordingEventBus) IsConnected() bool { return true } - -func TestApplyVoiceMode(t *testing.T) { - t.Run("nil value leaves settings unchanged", func(t *testing.T) { - settings := &models.UserSettings{ - VoiceMode: models.VoiceModeSettings{Engine: "webSpeech", Language: "en-US"}, - } - if err := applyVoiceMode(settings, nil); err != nil { - t.Fatalf("unexpected error: %v", err) - } - if settings.VoiceMode.Engine != "webSpeech" || settings.VoiceMode.Language != "en-US" { - t.Fatalf("expected unchanged, got %+v", settings.VoiceMode) - } - }) - - t.Run("happy path: applies a full update", func(t *testing.T) { - settings := &models.UserSettings{} - err := applyVoiceMode(settings, &models.VoiceModeSettings{ - Enabled: true, - Engine: "whisperWeb", - Language: "pt-PT", - Mode: "hold", - AutoSend: true, - WhisperWebModel: "small", - }) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - want := models.VoiceModeSettings{ - Enabled: true, - Engine: "whisperWeb", - Language: "pt-PT", - Mode: "hold", - AutoSend: true, - WhisperWebModel: "small", - } - if settings.VoiceMode != want { - t.Fatalf("expected %+v, got %+v", want, settings.VoiceMode) - } - }) - - t.Run("enabled=false is honored (user disabled the feature)", func(t *testing.T) { - settings := &models.UserSettings{VoiceMode: models.VoiceModeSettings{Enabled: true}} - if err := applyVoiceMode(settings, &models.VoiceModeSettings{Enabled: false}); err != nil { - t.Fatalf("unexpected error: %v", err) - } - if settings.VoiceMode.Enabled { - t.Fatalf("expected Enabled=false after disable, got true") - } - }) - - t.Run("invalid engine is rejected", func(t *testing.T) { - err := applyVoiceMode(&models.UserSettings{}, &models.VoiceModeSettings{Engine: "bogus"}) - if err == nil || !strings.Contains(err.Error(), "voice_mode.engine") { - t.Fatalf("expected engine validation error, got %v", err) - } - }) - - t.Run("invalid mode is rejected", func(t *testing.T) { - err := applyVoiceMode(&models.UserSettings{}, &models.VoiceModeSettings{Mode: "tap"}) - if err == nil || !strings.Contains(err.Error(), "voice_mode.mode") { - t.Fatalf("expected mode validation error, got %v", err) - } - }) - - t.Run("invalid whisper_web_model is rejected", func(t *testing.T) { - err := applyVoiceMode(&models.UserSettings{}, &models.VoiceModeSettings{WhisperWebModel: "huge"}) - if err == nil || !strings.Contains(err.Error(), "voice_mode.whisper_web_model") { - t.Fatalf("expected model validation error, got %v", err) - } - }) - - t.Run("partial update preserves string fields but zeroes booleans", func(t *testing.T) { - settings := &models.UserSettings{ - VoiceMode: models.VoiceModeSettings{ - Enabled: true, - Engine: "whisperServer", - Language: "en-GB", - Mode: "toggle", - AutoSend: true, - WhisperWebModel: "tiny", - }, - } - // Empty strings on the new payload mean "no change" for the string fields, - // but bools have no "unset" sentinel — every PATCH carries them. The settings - // UI always sends the full VoiceMode object so partial updates here would - // only happen in test or hand-crafted requests; the assertions below lock in - // that explicit behavior so it doesn't drift silently. - err := applyVoiceMode(settings, &models.VoiceModeSettings{Engine: "webSpeech"}) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if settings.VoiceMode.Engine != "webSpeech" { - t.Fatalf("expected engine=webSpeech, got %q", settings.VoiceMode.Engine) - } - if settings.VoiceMode.Language != "en-GB" { - t.Fatalf("expected language preserved, got %q", settings.VoiceMode.Language) - } - if settings.VoiceMode.Mode != "toggle" { - t.Fatalf("expected mode preserved, got %q", settings.VoiceMode.Mode) - } - if settings.VoiceMode.WhisperWebModel != "tiny" { - t.Fatalf("expected whisper model preserved, got %q", settings.VoiceMode.WhisperWebModel) - } - if settings.VoiceMode.Enabled { - t.Fatalf("expected Enabled zeroed on partial update, got true") - } - if settings.VoiceMode.AutoSend { - t.Fatalf("expected AutoSend zeroed on partial update, got true") - } - }) -} diff --git a/apps/backend/internal/user/store/sqlite.go b/apps/backend/internal/user/store/sqlite.go index 538480b812..59f0f3f79f 100644 --- a/apps/backend/internal/user/store/sqlite.go +++ b/apps/backend/internal/user/store/sqlite.go @@ -614,7 +614,6 @@ func marshalUserSettingsPayload(settings *models.UserSettings) ([]byte, error) { "system_metrics_display": settings.SystemMetricsDisplay, "app_status_bar_enabled": settings.AppStatusBarEnabled, "app_status_bar_order": normalizeAppStatusBarOrder(settings.AppStatusBarOrder), - "voice_mode": settings.VoiceMode, "kanban_hidden_step_ids": settings.KanbanHiddenStepIDs, }) } @@ -638,58 +637,6 @@ func scanUser(scanner interface{ Scan(dest ...any) error }) (*models.User, error return user, nil } -// defaultVoiceModeSettings returns the baseline VoiceMode configuration for -// users with no saved preferences. Mirrored on the frontend; keep in sync. -func defaultVoiceModeSettings() models.VoiceModeSettings { - return models.VoiceModeSettings{ - Enabled: true, - Engine: "auto", - Language: "auto", - Mode: "toggle", - AutoSend: false, - WhisperWebModel: "base", - } -} - -// storedVoiceMode is the on-disk JSON shape — uses *bool for `enabled` so we -// can distinguish "absent" (older rows written before the toggle existed — -// must default to true) from "explicitly false" (user disabled the feature). -type storedVoiceMode struct { - Enabled *bool `json:"enabled"` - Engine string `json:"engine"` - Language string `json:"language"` - Mode string `json:"mode"` - AutoSend bool `json:"auto_send"` - WhisperWebModel string `json:"whisper_web_model"` -} - -// mergeVoiceModeDefaults fills in zero/missing fields on a stored VoiceMode -// payload so older user rows (written before VoiceMode existed) still produce -// usable settings instead of empty strings the frontend would reject. -func mergeVoiceModeDefaults(stored *storedVoiceMode) models.VoiceModeSettings { - out := defaultVoiceModeSettings() - if stored == nil { - return out - } - if stored.Enabled != nil { - out.Enabled = *stored.Enabled - } - if stored.Engine != "" { - out.Engine = stored.Engine - } - if stored.Language != "" { - out.Language = stored.Language - } - if stored.Mode != "" { - out.Mode = stored.Mode - } - if stored.WhisperWebModel != "" { - out.WhisperWebModel = stored.WhisperWebModel - } - out.AutoSend = stored.AutoSend - return out -} - // defaultUserSettings returns the baseline settings for a user with no saved // preferences. func defaultUserSettings(userID string) *models.UserSettings { @@ -725,7 +672,6 @@ func defaultUserSettings(userID string) *models.UserSettings { SidebarTaskPrefs: normalizeSidebarTaskPrefs(models.SidebarTaskPrefs{}), AppStatusBarEnabled: false, AppStatusBarOrder: normalizeAppStatusBarOrder(models.AppStatusBarOrder{}), - VoiceMode: defaultVoiceModeSettings(), KanbanHiddenStepIDs: map[string][]string{}, } } @@ -808,7 +754,6 @@ func scanUserSettings(scanner interface{ Scan(dest ...any) error }, userID strin SystemMetricsDisplay models.SystemMetricsDisplaySettings `json:"system_metrics_display"` AppStatusBarEnabled *bool `json:"app_status_bar_enabled"` AppStatusBarOrder models.AppStatusBarOrder `json:"app_status_bar_order"` - VoiceMode *storedVoiceMode `json:"voice_mode"` KanbanHiddenStepIDs json.RawMessage `json:"kanban_hidden_step_ids"` } if err := json.Unmarshal([]byte(settingsRaw), &payload); err != nil { @@ -929,7 +874,6 @@ func scanUserSettings(scanner interface{ Scan(dest ...any) error }, userID strin } settings.TerminalFontFamily = payload.TerminalFontFamily settings.TerminalFontSize = payload.TerminalFontSize - settings.VoiceMode = mergeVoiceModeDefaults(payload.VoiceMode) settings.SystemMetricsDisplay = payload.SystemMetricsDisplay if payload.AppStatusBarEnabled != nil { settings.AppStatusBarEnabled = *payload.AppStatusBarEnabled diff --git a/apps/backend/internal/voice/handlers/transcribe_handlers.go b/apps/backend/internal/voice/handlers/transcribe_handlers.go deleted file mode 100644 index bb9992e3a2..0000000000 --- a/apps/backend/internal/voice/handlers/transcribe_handlers.go +++ /dev/null @@ -1,117 +0,0 @@ -// Package handlers exposes the HTTP surface for the voice-input transcription -// fallback. The endpoint is unauthenticated (matches /api/v1/features) — the -// Web Speech API path is preferred by the frontend, so this server-side -// fallback only runs when the browser cannot do it locally. -package handlers - -import ( - "errors" - "io" - "net/http" - - "github.com/gin-gonic/gin" - "go.uber.org/zap" - - "github.com/kandev/kandev/internal/common/logger" - "github.com/kandev/kandev/internal/voice/transcribe" -) - -// maxAudioBytes caps the multipart audio payload. Whisper accepts up to 25 MB -// per request; we cap lower so a stuck mic doesn't blow up backend memory or -// burn API spend on a stuck recording. -const maxAudioBytes = 10 * 1024 * 1024 - -// Handlers wires the transcribe service into Gin routes. -type Handlers struct { - svc *transcribe.Service - log *logger.Logger -} - -// NewHandlers constructs a Handlers from a transcribe Service. -func NewHandlers(svc *transcribe.Service, log *logger.Logger) *Handlers { - return &Handlers{ - svc: svc, - log: log.WithFields(zap.String("component", "voice-handlers")), - } -} - -// RegisterRoutes mounts the voice transcription endpoint. -func RegisterRoutes(router *gin.Engine, svc *transcribe.Service, log *logger.Logger) { - h := NewHandlers(svc, log) - api := router.Group("/api/v1") - api.POST("/transcribe", h.httpTranscribe) -} - -func (h *Handlers) httpTranscribe(c *gin.Context) { - if h.svc == nil || !h.svc.Configured() { - c.JSON(http.StatusServiceUnavailable, gin.H{ - "error": "voice transcription is not configured on this server", - }) - return - } - - // MaxBytesReader caps multipart parsing — once the cap is exceeded, Gin's - // multipart parser surfaces *http.MaxBytesError out of c.FormFile (because - // it reads the whole body through the wrapped reader before we ever get - // the *FileHeader). We need to distinguish that case from a genuinely - // missing field so the client sees 413 instead of a misleading 400. - c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, maxAudioBytes) - - fh, err := c.FormFile("audio") - if err != nil { - var maxBytesErr *http.MaxBytesError - if errors.As(err, &maxBytesErr) { - c.JSON(http.StatusRequestEntityTooLarge, gin.H{"error": "audio payload too large"}) - return - } - c.JSON(http.StatusBadRequest, gin.H{"error": "audio file is required (multipart field 'audio')"}) - return - } - - file, err := fh.Open() - if err != nil { - h.log.Warn("open uploaded audio failed", zap.Error(err)) - c.JSON(http.StatusBadRequest, gin.H{"error": "cannot open uploaded audio"}) - return - } - defer func() { _ = file.Close() }() - - data, err := io.ReadAll(file) - if err != nil { - h.log.Warn("read uploaded audio failed", zap.Error(err)) - c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to read uploaded audio"}) - return - } - if len(data) == 0 { - c.JSON(http.StatusBadRequest, gin.H{"error": "audio file is empty"}) - return - } - - mime := fh.Header.Get("Content-Type") - text, err := h.svc.Transcribe(c.Request.Context(), data, mime, fh.Filename) - if err != nil { - h.respondError(c, err) - return - } - c.JSON(http.StatusOK, gin.H{"text": text}) -} - -func (h *Handlers) respondError(c *gin.Context, err error) { - if errors.Is(err, transcribe.ErrNotConfigured) { - c.JSON(http.StatusServiceUnavailable, gin.H{ - "error": "voice transcription is not configured on this server", - }) - return - } - var upstream *transcribe.UpstreamError - if errors.As(err, &upstream) { - h.log.Warn("whisper upstream error", - zap.Int("status", upstream.StatusCode), - zap.String("body", upstream.Body), - ) - c.JSON(http.StatusBadGateway, gin.H{"error": "upstream transcription error"}) - return - } - h.log.Error("transcription failed", zap.Error(err)) - c.JSON(http.StatusInternalServerError, gin.H{"error": "transcription failed"}) -} diff --git a/apps/backend/internal/voice/handlers/transcribe_handlers_test.go b/apps/backend/internal/voice/handlers/transcribe_handlers_test.go deleted file mode 100644 index aa3170578b..0000000000 --- a/apps/backend/internal/voice/handlers/transcribe_handlers_test.go +++ /dev/null @@ -1,157 +0,0 @@ -package handlers - -import ( - "bytes" - "encoding/json" - "mime/multipart" - "net/http" - "net/http/httptest" - "strings" - "testing" - - "github.com/gin-gonic/gin" - - "github.com/kandev/kandev/internal/common/logger" - "github.com/kandev/kandev/internal/voice/transcribe" -) - -func init() { - gin.SetMode(gin.TestMode) -} - -func testLogger(t *testing.T) *logger.Logger { - t.Helper() - log, err := logger.NewLogger(logger.LoggingConfig{Level: "error", Format: "text", OutputPath: "stderr"}) - if err != nil { - t.Fatalf("logger.NewLogger: %v", err) - } - return log -} - -func buildAudioRequest(t *testing.T, field, filename, mime string, data []byte) (*http.Request, string) { - t.Helper() - buf := &bytes.Buffer{} - w := multipart.NewWriter(buf) - if data != nil { - fw, err := createFormFile(w, field, filename, mime) - if err != nil { - t.Fatal(err) - } - _, _ = fw.Write(data) - } - _ = w.Close() - req := httptest.NewRequest(http.MethodPost, "/api/v1/transcribe", buf) - req.Header.Set("Content-Type", w.FormDataContentType()) - return req, w.FormDataContentType() -} - -func createFormFile(w *multipart.Writer, field, filename, mime string) (interface{ Write([]byte) (int, error) }, error) { - if mime == "" { - return w.CreateFormFile(field, filename) - } - hdr := make(map[string][]string) - hdr["Content-Disposition"] = []string{"form-data; name=\"" + field + "\"; filename=\"" + filename + "\""} - hdr["Content-Type"] = []string{mime} - return w.CreatePart(hdr) -} - -func newRouter(svc *transcribe.Service, t *testing.T) *gin.Engine { - r := gin.New() - RegisterRoutes(r, svc, testLogger(t)) - return r -} - -func TestTranscribe_NotConfigured(t *testing.T) { - svc := transcribe.New("") - r := newRouter(svc, t) - - req, _ := buildAudioRequest(t, "audio", "a.webm", "audio/webm", []byte("hello")) - w := httptest.NewRecorder() - r.ServeHTTP(w, req) - - if w.Code != http.StatusServiceUnavailable { - t.Fatalf("status = %d, want 503; body=%s", w.Code, w.Body.String()) - } -} - -func TestTranscribe_MissingFile(t *testing.T) { - svc := transcribe.New("sk-test") - r := newRouter(svc, t) - - // No file part — just an empty form. - buf := &bytes.Buffer{} - w := multipart.NewWriter(buf) - _ = w.Close() - req := httptest.NewRequest(http.MethodPost, "/api/v1/transcribe", buf) - req.Header.Set("Content-Type", w.FormDataContentType()) - - rr := httptest.NewRecorder() - r.ServeHTTP(rr, req) - if rr.Code != http.StatusBadRequest { - t.Fatalf("status = %d, want 400; body=%s", rr.Code, rr.Body.String()) - } -} - -func TestTranscribe_EmptyAudio(t *testing.T) { - svc := transcribe.New("sk-test") - r := newRouter(svc, t) - - req, _ := buildAudioRequest(t, "audio", "a.webm", "audio/webm", []byte{}) - rr := httptest.NewRecorder() - r.ServeHTTP(rr, req) - if rr.Code != http.StatusBadRequest { - t.Fatalf("status = %d, want 400; body=%s", rr.Code, rr.Body.String()) - } -} - -func TestTranscribe_Success(t *testing.T) { - upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Header.Get("Authorization") != "Bearer sk-test" { - t.Errorf("auth header missing") - } - w.Header().Set("Content-Type", "application/json") - _, _ = w.Write([]byte(`{"text":"transcribed"}`)) - })) - defer upstream.Close() - - svc := transcribe.New("sk-test", transcribe.WithEndpoint(upstream.URL)) - r := newRouter(svc, t) - - req, _ := buildAudioRequest(t, "audio", "clip.webm", "audio/webm", []byte("bytes")) - rr := httptest.NewRecorder() - r.ServeHTTP(rr, req) - - if rr.Code != http.StatusOK { - t.Fatalf("status = %d, want 200; body=%s", rr.Code, rr.Body.String()) - } - var body struct { - Text string `json:"text"` - } - if err := json.Unmarshal(rr.Body.Bytes(), &body); err != nil { - t.Fatal(err) - } - if body.Text != "transcribed" { - t.Errorf("text = %q", body.Text) - } -} - -func TestTranscribe_UpstreamError(t *testing.T) { - upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusBadGateway) - _, _ = w.Write([]byte(`{"error":"oops"}`)) - })) - defer upstream.Close() - - svc := transcribe.New("sk-test", transcribe.WithEndpoint(upstream.URL)) - r := newRouter(svc, t) - req, _ := buildAudioRequest(t, "audio", "a.webm", "audio/webm", []byte("bytes")) - rr := httptest.NewRecorder() - r.ServeHTTP(rr, req) - - if rr.Code != http.StatusBadGateway { - t.Fatalf("status = %d, want 502; body=%s", rr.Code, rr.Body.String()) - } - if !strings.Contains(rr.Body.String(), "upstream") { - t.Errorf("body should mention upstream: %s", rr.Body.String()) - } -} diff --git a/apps/backend/internal/voice/transcribe/service.go b/apps/backend/internal/voice/transcribe/service.go deleted file mode 100644 index fad7b13b90..0000000000 --- a/apps/backend/internal/voice/transcribe/service.go +++ /dev/null @@ -1,185 +0,0 @@ -// Package transcribe wraps the OpenAI Whisper transcription endpoint for the -// chat voice-input fallback. The browser's Web Speech API is the primary -// voice-input engine; this server-side path is only hit when the browser -// has no SpeechRecognition support. -package transcribe - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "io" - "mime/multipart" - "net/http" - "net/textproto" - "strings" - "time" -) - -// ErrNotConfigured is returned when the service has no API key — the handler -// maps this to HTTP 503 so the frontend can hide the Whisper fallback path -// instead of repeatedly retrying a deployment that will never succeed. -var ErrNotConfigured = errors.New("voice transcription is not configured") - -// UpstreamError wraps a non-2xx response from OpenAI so the handler can map -// it to HTTP 502 and surface a clean error to the caller. -type UpstreamError struct { - StatusCode int - Body string -} - -func (e *UpstreamError) Error() string { - return fmt.Sprintf("openai whisper upstream error: status=%d body=%s", e.StatusCode, e.Body) -} - -const ( - defaultEndpoint = "https://api.openai.com/v1/audio/transcriptions" - defaultModel = "whisper-1" - defaultTimeout = 60 * time.Second -) - -// Service transcribes audio via OpenAI's Whisper endpoint. -type Service struct { - apiKey string - endpoint string - model string - client *http.Client -} - -// Option customises a Service for tests (custom endpoint, HTTP client). -type Option func(*Service) - -// WithEndpoint overrides the upstream URL — used by tests with httptest servers. -func WithEndpoint(url string) Option { - return func(s *Service) { s.endpoint = url } -} - -// WithHTTPClient overrides the HTTP client. -func WithHTTPClient(c *http.Client) Option { - return func(s *Service) { s.client = c } -} - -// WithModel overrides the Whisper model name. -func WithModel(model string) Option { - return func(s *Service) { s.model = model } -} - -// New constructs a Service. apiKey may be empty; in that case Transcribe -// returns ErrNotConfigured without making any network calls. -func New(apiKey string, opts ...Option) *Service { - s := &Service{ - apiKey: apiKey, - endpoint: defaultEndpoint, - model: defaultModel, - client: &http.Client{Timeout: defaultTimeout}, - } - for _, o := range opts { - o(s) - } - return s -} - -// Configured reports whether the service has an API key. Used by handlers -// to short-circuit before reading the request body. -func (s *Service) Configured() bool { - return s != nil && strings.TrimSpace(s.apiKey) != "" -} - -// Transcribe sends the given audio bytes to OpenAI Whisper and returns the -// transcribed text. filename is used for the multipart Content-Disposition; -// Whisper relies on the file extension to detect the audio format. -func (s *Service) Transcribe(ctx context.Context, audio []byte, mimeType, filename string) (string, error) { - if !s.Configured() { - return "", ErrNotConfigured - } - if len(audio) == 0 { - return "", errors.New("audio payload is empty") - } - - body, contentType, err := buildMultipart(audio, mimeType, filename, s.model) - if err != nil { - return "", fmt.Errorf("build multipart body: %w", err) - } - - req, err := http.NewRequestWithContext(ctx, http.MethodPost, s.endpoint, body) - if err != nil { - return "", fmt.Errorf("build whisper request: %w", err) - } - req.Header.Set("Authorization", "Bearer "+s.apiKey) - req.Header.Set("Content-Type", contentType) - req.Header.Set("Accept", "application/json") - - resp, err := s.client.Do(req) - if err != nil { - return "", fmt.Errorf("call whisper endpoint: %w", err) - } - defer func() { _ = resp.Body.Close() }() - - rawBody, _ := io.ReadAll(resp.Body) - if resp.StatusCode < 200 || resp.StatusCode >= 300 { - return "", &UpstreamError{StatusCode: resp.StatusCode, Body: string(rawBody)} - } - - var parsed struct { - Text string `json:"text"` - } - if err := json.Unmarshal(rawBody, &parsed); err != nil { - return "", fmt.Errorf("decode whisper response: %w", err) - } - return strings.TrimSpace(parsed.Text), nil -} - -// buildMultipart assembles the multipart/form-data body Whisper expects: -// `file`, `model`, and `response_format=json`. -func buildMultipart(audio []byte, mimeType, filename, model string) (io.Reader, string, error) { - buf := &bytes.Buffer{} - w := multipart.NewWriter(buf) - - if filename == "" { - filename = "recording" + extensionForMime(mimeType) - } - header := textproto.MIMEHeader{} - header.Set("Content-Disposition", fmt.Sprintf(`form-data; name="file"; filename=%q`, filename)) - if mimeType != "" { - header.Set("Content-Type", mimeType) - } - filePart, err := w.CreatePart(header) - if err != nil { - return nil, "", err - } - if _, err := filePart.Write(audio); err != nil { - return nil, "", err - } - - if err := w.WriteField("model", model); err != nil { - return nil, "", err - } - if err := w.WriteField("response_format", "json"); err != nil { - return nil, "", err - } - if err := w.Close(); err != nil { - return nil, "", err - } - return buf, w.FormDataContentType(), nil -} - -// extensionForMime maps the audio MIME types MediaRecorder commonly emits to -// the file extensions Whisper recognises. Default to ".webm" — supported by -// Whisper and the most common MediaRecorder default on Chrome. -func extensionForMime(mime string) string { - mime = strings.ToLower(mime) - switch { - case strings.Contains(mime, "wav"): - return ".wav" - case strings.Contains(mime, "mp4"), strings.Contains(mime, "m4a"): - return ".m4a" - case strings.Contains(mime, "mpeg"), strings.Contains(mime, "mp3"): - return ".mp3" - case strings.Contains(mime, "ogg"): - return ".ogg" - default: - return ".webm" - } -} diff --git a/apps/backend/internal/voice/transcribe/service_test.go b/apps/backend/internal/voice/transcribe/service_test.go deleted file mode 100644 index b8c8315cf3..0000000000 --- a/apps/backend/internal/voice/transcribe/service_test.go +++ /dev/null @@ -1,222 +0,0 @@ -package transcribe - -import ( - "context" - "errors" - "io" - "mime/multipart" - "net/http" - "net/http/httptest" - "strings" - "testing" -) - -func TestService_Transcribe_NotConfigured(t *testing.T) { - svc := New("") - _, err := svc.Transcribe(context.Background(), []byte("data"), "audio/webm", "") - if !errors.Is(err, ErrNotConfigured) { - t.Fatalf("expected ErrNotConfigured, got %v", err) - } -} - -func TestService_Configured(t *testing.T) { - if New("").Configured() { - t.Errorf("empty key should not be configured") - } - if New(" ").Configured() { - t.Errorf("whitespace-only key should not be configured") - } - if !New("sk-test").Configured() { - t.Errorf("non-empty key should be configured") - } -} - -func TestService_Transcribe_EmptyAudio(t *testing.T) { - svc := New("sk-test") - _, err := svc.Transcribe(context.Background(), nil, "audio/webm", "") - if err == nil { - t.Fatal("expected error for empty audio") - } -} - -func TestService_Transcribe_Success(t *testing.T) { - var capturedAuth string - var capturedFilename string - var capturedFileBytes []byte - var capturedModel string - var capturedFormat string - - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - capturedAuth = r.Header.Get("Authorization") - if err := r.ParseMultipartForm(32 << 20); err != nil { - t.Errorf("parse multipart: %v", err) - } - capturedModel = r.FormValue("model") - capturedFormat = r.FormValue("response_format") - // Use Errorf + return inside the HTTP handler goroutine — t.Fatalf - // from a non-test goroutine triggers FailNow which panics rather than - // failing the test cleanly. - fh := r.MultipartForm.File["file"] - if len(fh) != 1 { - t.Errorf("expected 1 file part, got %d", len(fh)) - return - } - capturedFilename = fh[0].Filename - f, err := fh[0].Open() - if err != nil { - t.Errorf("open file: %v", err) - return - } - defer func() { _ = f.Close() }() - capturedFileBytes, _ = io.ReadAll(f) - w.Header().Set("Content-Type", "application/json") - _, _ = w.Write([]byte(`{"text":"hello world"}`)) - })) - defer srv.Close() - - svc := New("sk-test", WithEndpoint(srv.URL)) - text, err := svc.Transcribe(context.Background(), []byte("audio-bytes"), "audio/webm", "clip.webm") - if err != nil { - t.Fatalf("Transcribe failed: %v", err) - } - if text != "hello world" { - t.Errorf("unexpected text: %q", text) - } - if capturedAuth != "Bearer sk-test" { - t.Errorf("auth header = %q", capturedAuth) - } - if capturedModel != defaultModel { - t.Errorf("model = %q", capturedModel) - } - if capturedFormat != "json" { - t.Errorf("response_format = %q", capturedFormat) - } - if capturedFilename != "clip.webm" { - t.Errorf("filename = %q", capturedFilename) - } - if string(capturedFileBytes) != "audio-bytes" { - t.Errorf("file body = %q", string(capturedFileBytes)) - } -} - -func TestService_Transcribe_DerivedFilename(t *testing.T) { - var capturedFilename string - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - _ = r.ParseMultipartForm(32 << 20) - fh := r.MultipartForm.File["file"] - if len(fh) == 1 { - capturedFilename = fh[0].Filename - } - _, _ = w.Write([]byte(`{"text":""}`)) - })) - defer srv.Close() - - svc := New("sk-test", WithEndpoint(srv.URL)) - _, err := svc.Transcribe(context.Background(), []byte("a"), "audio/wav", "") - if err != nil { - t.Fatal(err) - } - if !strings.HasSuffix(capturedFilename, ".wav") { - t.Errorf("derived filename should use .wav for audio/wav, got %q", capturedFilename) - } -} - -func TestService_Transcribe_UpstreamError(t *testing.T) { - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusBadRequest) - _, _ = w.Write([]byte(`{"error":"bad audio"}`)) - })) - defer srv.Close() - - svc := New("sk-test", WithEndpoint(srv.URL)) - _, err := svc.Transcribe(context.Background(), []byte("a"), "audio/webm", "") - var upstream *UpstreamError - if !errors.As(err, &upstream) { - t.Fatalf("expected UpstreamError, got %T: %v", err, err) - } - if upstream.StatusCode != http.StatusBadRequest { - t.Errorf("status = %d", upstream.StatusCode) - } - if !strings.Contains(upstream.Body, "bad audio") { - t.Errorf("body did not contain upstream payload: %q", upstream.Body) - } -} - -func TestExtensionForMime(t *testing.T) { - cases := map[string]string{ - "audio/webm": ".webm", - "audio/wav": ".wav", - "audio/x-wav": ".wav", - "audio/mp4": ".m4a", - "audio/m4a": ".m4a", - "audio/mpeg": ".mp3", - "audio/mp3": ".mp3", - "audio/ogg": ".ogg", - "": ".webm", - "application/anything": ".webm", - } - for mime, want := range cases { - if got := extensionForMime(mime); got != want { - t.Errorf("extensionForMime(%q) = %q, want %q", mime, got, want) - } - } -} - -func TestBuildMultipart_Roundtrip(t *testing.T) { - body, ct, err := buildMultipart([]byte("hello"), "audio/wav", "a.wav", "whisper-1") - if err != nil { - t.Fatal(err) - } - // Parse the multipart body back out using the boundary embedded in ct. - mediaType, params, ok := splitContentType(ct) - if !ok || mediaType != "multipart/form-data" { - t.Fatalf("unexpected content-type: %q", ct) - } - mr := multipart.NewReader(body, params["boundary"]) - fields := map[string]string{} - var fileContent string - for { - part, err := mr.NextPart() - if err == io.EOF { - break - } - if err != nil { - t.Fatal(err) - } - buf, _ := io.ReadAll(part) - if part.FileName() != "" { - fileContent = string(buf) - } else { - fields[part.FormName()] = string(buf) - } - } - if fileContent != "hello" { - t.Errorf("file part = %q", fileContent) - } - if fields["model"] != "whisper-1" { - t.Errorf("model field = %q", fields["model"]) - } - if fields["response_format"] != "json" { - t.Errorf("response_format field = %q", fields["response_format"]) - } -} - -// splitContentType is a tiny helper to split "multipart/form-data; boundary=…" -// without pulling in mime.ParseMediaType — keeps this test file self-contained. -func splitContentType(ct string) (string, map[string]string, bool) { - parts := strings.SplitN(ct, ";", 2) - if len(parts) != 2 { - return "", nil, false - } - mediaType := strings.TrimSpace(parts[0]) - params := map[string]string{} - for _, kv := range strings.Split(parts[1], ";") { - kv = strings.TrimSpace(kv) - eq := strings.IndexByte(kv, '=') - if eq < 0 { - continue - } - params[kv[:eq]] = strings.Trim(kv[eq+1:], `"`) - } - return mediaType, params, true -} diff --git a/apps/pnpm-lock.yaml b/apps/pnpm-lock.yaml index 28e21d53b5..08a59996c6 100644 --- a/apps/pnpm-lock.yaml +++ b/apps/pnpm-lock.yaml @@ -273,9 +273,6 @@ importers: '@dnd-kit/utilities': specifier: ^3.2.2 version: 3.2.2(react@19.2.3) - '@huggingface/transformers': - specifier: ^4.2.0 - version: 4.2.0(@types/node@20.19.28) '@kandev/theme': specifier: workspace:* version: link:../packages/theme @@ -1182,16 +1179,6 @@ packages: peerDependencies: hono: ^4.12.34 - '@huggingface/jinja@0.5.9': - resolution: {integrity: sha512-uWTG+l3VJRsl7EXxYizuL3P+cCPoc3cRqbWWRcQN0FhejRfbdq0RNhCmbY/YDtnTcz9icdLYuLDjsnz4d8JMuw==} - engines: {node: '>=18'} - - '@huggingface/tokenizers@0.1.3': - resolution: {integrity: sha512-8rF/RRT10u+kn7YuUbUg0OF30K8rjTc78aHpxT+qJ1uWSqxT1MHi8+9ltwYfkFYJzT/oS+qw3JVfHtNMGAdqyA==} - - '@huggingface/transformers@4.2.0': - resolution: {integrity: sha512-8BRCoBMH0XsWaEIamuR0LrJGAfftgHAfb2Vrffy0VKlSAE/MnUJ5/h/zTfEP3fDIft+nk7TqB8xXEyABGitBjQ==} - '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -1214,152 +1201,6 @@ packages: '@iconify/utils@3.1.0': resolution: {integrity: sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==} - '@img/colour@1.1.0': - resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} - engines: {node: '>=18'} - - '@img/sharp-darwin-arm64@0.35.3': - resolution: {integrity: sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==} - engines: {node: '>=20.9.0'} - cpu: [arm64] - os: [darwin] - - '@img/sharp-darwin-x64@0.35.3': - resolution: {integrity: sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==} - engines: {node: '>=20.9.0'} - cpu: [x64] - os: [darwin] - - '@img/sharp-freebsd-wasm32@0.35.3': - resolution: {integrity: sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==} - engines: {node: '>=20.9.0'} - os: [freebsd] - - '@img/sharp-libvips-darwin-arm64@1.3.2': - resolution: {integrity: sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==} - cpu: [arm64] - os: [darwin] - - '@img/sharp-libvips-darwin-x64@1.3.2': - resolution: {integrity: sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==} - cpu: [x64] - os: [darwin] - - '@img/sharp-libvips-linux-arm64@1.3.2': - resolution: {integrity: sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==} - cpu: [arm64] - os: [linux] - - '@img/sharp-libvips-linux-arm@1.3.2': - resolution: {integrity: sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==} - cpu: [arm] - os: [linux] - - '@img/sharp-libvips-linux-ppc64@1.3.2': - resolution: {integrity: sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==} - cpu: [ppc64] - os: [linux] - - '@img/sharp-libvips-linux-riscv64@1.3.2': - resolution: {integrity: sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==} - cpu: [riscv64] - os: [linux] - - '@img/sharp-libvips-linux-s390x@1.3.2': - resolution: {integrity: sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==} - cpu: [s390x] - os: [linux] - - '@img/sharp-libvips-linux-x64@1.3.2': - resolution: {integrity: sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==} - cpu: [x64] - os: [linux] - - '@img/sharp-libvips-linuxmusl-arm64@1.3.2': - resolution: {integrity: sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==} - cpu: [arm64] - os: [linux] - - '@img/sharp-libvips-linuxmusl-x64@1.3.2': - resolution: {integrity: sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==} - cpu: [x64] - os: [linux] - - '@img/sharp-linux-arm64@0.35.3': - resolution: {integrity: sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==} - engines: {node: '>=20.9.0'} - cpu: [arm64] - os: [linux] - - '@img/sharp-linux-arm@0.35.3': - resolution: {integrity: sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==} - engines: {node: '>=20.9.0'} - cpu: [arm] - os: [linux] - - '@img/sharp-linux-ppc64@0.35.3': - resolution: {integrity: sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==} - engines: {node: '>=20.9.0'} - cpu: [ppc64] - os: [linux] - - '@img/sharp-linux-riscv64@0.35.3': - resolution: {integrity: sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==} - engines: {node: '>=20.9.0'} - cpu: [riscv64] - os: [linux] - - '@img/sharp-linux-s390x@0.35.3': - resolution: {integrity: sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==} - engines: {node: '>=20.9.0'} - cpu: [s390x] - os: [linux] - - '@img/sharp-linux-x64@0.35.3': - resolution: {integrity: sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==} - engines: {node: '>=20.9.0'} - cpu: [x64] - os: [linux] - - '@img/sharp-linuxmusl-arm64@0.35.3': - resolution: {integrity: sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==} - engines: {node: '>=20.9.0'} - cpu: [arm64] - os: [linux] - - '@img/sharp-linuxmusl-x64@0.35.3': - resolution: {integrity: sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==} - engines: {node: '>=20.9.0'} - cpu: [x64] - os: [linux] - - '@img/sharp-wasm32@0.35.3': - resolution: {integrity: sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==} - engines: {node: '>=20.9.0'} - - '@img/sharp-webcontainers-wasm32@0.35.3': - resolution: {integrity: sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==} - engines: {node: '>=20.9.0'} - cpu: [wasm32] - - '@img/sharp-win32-arm64@0.35.3': - resolution: {integrity: sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==} - engines: {node: '>=20.9.0'} - cpu: [arm64] - os: [win32] - - '@img/sharp-win32-ia32@0.35.3': - resolution: {integrity: sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==} - engines: {node: ^20.9.0} - cpu: [ia32] - os: [win32] - - '@img/sharp-win32-x64@0.35.3': - resolution: {integrity: sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==} - engines: {node: '>=20.9.0'} - cpu: [x64] - os: [win32] - '@inquirer/ansi@1.0.2': resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==} engines: {node: '>=18'} @@ -1790,33 +1631,6 @@ packages: engines: {node: '>=18'} hasBin: true - '@protobufjs/aspromise@1.1.2': - resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} - - '@protobufjs/base64@1.1.2': - resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} - - '@protobufjs/codegen@2.0.5': - resolution: {integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==} - - '@protobufjs/eventemitter@1.1.1': - resolution: {integrity: sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==} - - '@protobufjs/fetch@1.1.1': - resolution: {integrity: sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==} - - '@protobufjs/float@1.0.2': - resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} - - '@protobufjs/path@1.1.2': - resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} - - '@protobufjs/pool@1.1.0': - resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} - - '@protobufjs/utf8@1.1.1': - resolution: {integrity: sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==} - '@radix-ui/number@1.1.1': resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} @@ -3531,10 +3345,6 @@ packages: engines: {node: '>=0.4.0'} hasBin: true - adm-zip@0.6.0: - resolution: {integrity: sha512-XleryMhbuksdKtofnWZ9Sk+4CUTbms4Mb/EU32SZwToAyZ5RgVos/ki8n+yr0LWHOGKuakbXTuuYNHLQjhddgg==} - engines: {node: '>=14.0'} - agent-base@7.1.4: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} @@ -3612,10 +3422,6 @@ packages: resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==} engines: {node: '>=18'} - boolean@3.2.0: - resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} - deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. - brace-expansion@1.1.18: resolution: {integrity: sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==} @@ -4057,18 +3863,10 @@ packages: resolution: {integrity: sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg==} engines: {node: '>=18'} - define-data-property@1.1.4: - resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} - engines: {node: '>= 0.4'} - define-lazy-prop@3.0.0: resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} engines: {node: '>=12'} - define-properties@1.2.1: - resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} - engines: {node: '>= 0.4'} - delaunator@5.0.1: resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==} @@ -4087,9 +3885,6 @@ packages: detect-node-es@1.1.0: resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} - detect-node@2.1.0: - resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} - devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} @@ -4203,9 +3998,6 @@ packages: es-toolkit@1.50.0: resolution: {integrity: sha512-OyZKhUVvEep9ITEiwHn8GKnMRQIVqoSIX7WnRbkWgJkllCujilqP2rD0u979tkl8wqyc8ICwlc1UBVv/Sl1G6w==} - es6-error@4.1.1: - resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==} - esbuild@0.27.2: resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} engines: {node: '>=18'} @@ -4406,9 +4198,6 @@ packages: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} - flatbuffers@25.9.23: - resolution: {integrity: sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ==} - flatted@3.4.3: resolution: {integrity: sha512-/zipXxyO6rGvuNGDiULY9MvEGSkb2gaG4GGH4ygMi0ZZzyMHdUZBmntJmx5x1G2VuPytCwGN4xsJP6cw+sK+vQ==} @@ -4514,10 +4303,6 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} - global-agent@3.0.0: - resolution: {integrity: sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==} - engines: {node: '>=10.0'} - global-directory@4.0.1: resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} engines: {node: '>=18'} @@ -4526,10 +4311,6 @@ packages: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} - globalthis@1.0.4: - resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} - engines: {node: '>= 0.4'} - gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -4541,9 +4322,6 @@ packages: resolution: {integrity: sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} - guid-typescript@1.0.9: - resolution: {integrity: sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==} - hachure-fill@0.5.2: resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==} @@ -4555,9 +4333,6 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} - has-property-descriptors@1.0.2: - resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} - has-symbols@1.1.0: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} @@ -4846,9 +4621,6 @@ packages: json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - json-stringify-safe@5.0.1: - resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} - json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} @@ -5083,9 +4855,6 @@ packages: resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==} engines: {node: '>=18'} - long@5.3.2: - resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} - longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} @@ -5129,10 +4898,6 @@ packages: engines: {node: '>= 20'} hasBin: true - matcher@3.0.0: - resolution: {integrity: sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==} - engines: {node: '>=10'} - math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -5395,10 +5160,6 @@ packages: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} - object-keys@1.1.1: - resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} - engines: {node: '>= 0.4'} - object-treeify@1.1.33: resolution: {integrity: sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A==} engines: {node: '>= 10'} @@ -5428,19 +5189,6 @@ packages: oniguruma-to-es@4.3.4: resolution: {integrity: sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA==} - onnxruntime-common@1.24.0-dev.20251116-b39e144322: - resolution: {integrity: sha512-BOoomdHYmNRL5r4iQ4bMvsl2t0/hzVQ3OM3PHD0gxeXu1PmggqBv3puZicEUVOA3AtHHYmqZtjMj9FOfGrATTw==} - - onnxruntime-common@1.24.3: - resolution: {integrity: sha512-GeuPZO6U/LBJXvwdaqHbuUmoXiEdeCjWi/EG7Y1HNnDwJYuk6WUbNXpF6luSUY8yASul3cmUlLGrCCL1ZgVXqA==} - - onnxruntime-node@1.24.3: - resolution: {integrity: sha512-JH7+czbc8ALA819vlTgcV+Q214/+VjGeBHDjX81+ZCD0PCVCIFGFNtT0V4sXG/1JXypKPgScQcB3ij/hk3YnTg==} - os: [win32, darwin, linux] - - onnxruntime-web@1.26.0-dev.20260416-b7804b056c: - resolution: {integrity: sha512-MD6Ss4GSpQBo6zqoJzyT9LRbKYs7x/JVN23FT24EcEvlqF4VuzPOeH6X38orZPKHQDbprn7K+SBpu0/mj2CQiw==} - open@11.0.0: resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==} engines: {node: '>=20'} @@ -5544,9 +5292,6 @@ packages: pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} - platform@1.3.6: - resolution: {integrity: sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==} - playwright-core@1.61.1: resolution: {integrity: sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==} engines: {node: '>=18'} @@ -5670,10 +5415,6 @@ packages: prosemirror-view@1.41.5: resolution: {integrity: sha512-UDQbIPnDrjE8tqUBbPmCOZgtd75htE6W3r0JCmY9bL6W1iemDM37MZEKC49d+tdQ0v/CKx4gjxLoLsfkD2NiZA==} - protobufjs@7.6.5: - resolution: {integrity: sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==} - engines: {node: '>=12.0.0'} - proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -5901,10 +5642,6 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - roarr@2.15.4: - resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==} - engines: {node: '>=8.0'} - robust-predicates@3.0.2: resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==} @@ -5943,9 +5680,6 @@ packages: resolution: {integrity: sha512-3A6sD0WYP7+QrjbfNA2FN3FsOaGGFoekCVgTyypy53gPxhbkCIjtO6YWgdrfM+n/8sI8JeXZOIxsHjMTNxQ4nQ==} engines: {node: ^14.0.0 || >=16.0.0} - semver-compare@1.0.0: - resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==} - semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -5964,10 +5698,6 @@ packages: resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} engines: {node: '>= 18'} - serialize-error@7.0.1: - resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==} - engines: {node: '>=10'} - serve-static@2.2.1: resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} engines: {node: '>= 18'} @@ -5979,15 +5709,6 @@ packages: resolution: {integrity: sha512-j2xlma8PtYLbhvA612/MPOrDYsEp0DIiU1gC0BEbSBqWR6mBgwiKpA21Juq9tSswgUeIfxoUzZX8c7YwcL3ncA==} hasBin: true - sharp@0.35.3: - resolution: {integrity: sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==} - engines: {node: '>=20.9.0'} - peerDependencies: - '@types/node': '*' - peerDependenciesMeta: - '@types/node': - optional: true - shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -6053,9 +5774,6 @@ packages: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} - sprintf-js@1.1.3: - resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} - stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} @@ -6235,10 +5953,6 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} - type-fest@0.13.1: - resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==} - engines: {node: '>=10'} - type-fest@5.4.0: resolution: {integrity: sha512-wfkA6r0tBpVfGiyO+zbf9e10QkRQSlK9F2UvyfnjoCmrvH2bjHyhPzhugSBOuq1dog3P0+FKckqe+Xf6WKVjwg==} engines: {node: '>=20'} @@ -7336,20 +7050,6 @@ snapshots: dependencies: hono: 4.13.0 - '@huggingface/jinja@0.5.9': {} - - '@huggingface/tokenizers@0.1.3': {} - - '@huggingface/transformers@4.2.0(@types/node@20.19.28)': - dependencies: - '@huggingface/jinja': 0.5.9 - '@huggingface/tokenizers': 0.1.3 - onnxruntime-node: 1.24.3 - onnxruntime-web: 1.26.0-dev.20260416-b7804b056c - sharp: 0.35.3(@types/node@20.19.28) - transitivePeerDependencies: - - '@types/node' - '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.7': @@ -7369,112 +7069,6 @@ snapshots: '@iconify/types': 2.0.0 mlly: 1.8.0 - '@img/colour@1.1.0': {} - - '@img/sharp-darwin-arm64@0.35.3': - optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.3.2 - optional: true - - '@img/sharp-darwin-x64@0.35.3': - optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.3.2 - optional: true - - '@img/sharp-freebsd-wasm32@0.35.3': - dependencies: - '@img/sharp-wasm32': 0.35.3 - optional: true - - '@img/sharp-libvips-darwin-arm64@1.3.2': - optional: true - - '@img/sharp-libvips-darwin-x64@1.3.2': - optional: true - - '@img/sharp-libvips-linux-arm64@1.3.2': - optional: true - - '@img/sharp-libvips-linux-arm@1.3.2': - optional: true - - '@img/sharp-libvips-linux-ppc64@1.3.2': - optional: true - - '@img/sharp-libvips-linux-riscv64@1.3.2': - optional: true - - '@img/sharp-libvips-linux-s390x@1.3.2': - optional: true - - '@img/sharp-libvips-linux-x64@1.3.2': - optional: true - - '@img/sharp-libvips-linuxmusl-arm64@1.3.2': - optional: true - - '@img/sharp-libvips-linuxmusl-x64@1.3.2': - optional: true - - '@img/sharp-linux-arm64@0.35.3': - optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.3.2 - optional: true - - '@img/sharp-linux-arm@0.35.3': - optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.3.2 - optional: true - - '@img/sharp-linux-ppc64@0.35.3': - optionalDependencies: - '@img/sharp-libvips-linux-ppc64': 1.3.2 - optional: true - - '@img/sharp-linux-riscv64@0.35.3': - optionalDependencies: - '@img/sharp-libvips-linux-riscv64': 1.3.2 - optional: true - - '@img/sharp-linux-s390x@0.35.3': - optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.3.2 - optional: true - - '@img/sharp-linux-x64@0.35.3': - optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.3.2 - optional: true - - '@img/sharp-linuxmusl-arm64@0.35.3': - optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.3.2 - optional: true - - '@img/sharp-linuxmusl-x64@0.35.3': - optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.3.2 - optional: true - - '@img/sharp-wasm32@0.35.3': - dependencies: - '@emnapi/runtime': 1.11.2 - optional: true - - '@img/sharp-webcontainers-wasm32@0.35.3': - dependencies: - '@img/sharp-wasm32': 0.35.3 - optional: true - - '@img/sharp-win32-arm64@0.35.3': - optional: true - - '@img/sharp-win32-ia32@0.35.3': - optional: true - - '@img/sharp-win32-x64@0.35.3': - optional: true - '@inquirer/ansi@1.0.2': {} '@inquirer/confirm@5.1.21(@types/node@20.19.28)': @@ -7856,26 +7450,6 @@ snapshots: dependencies: playwright: 1.61.1 - '@protobufjs/aspromise@1.1.2': {} - - '@protobufjs/base64@1.1.2': {} - - '@protobufjs/codegen@2.0.5': {} - - '@protobufjs/eventemitter@1.1.1': {} - - '@protobufjs/fetch@1.1.1': - dependencies: - '@protobufjs/aspromise': 1.1.2 - - '@protobufjs/float@1.0.2': {} - - '@protobufjs/path@1.1.2': {} - - '@protobufjs/pool@1.1.0': {} - - '@protobufjs/utf8@1.1.1': {} - '@radix-ui/number@1.1.1': {} '@radix-ui/primitive@1.1.3': {} @@ -9627,8 +9201,6 @@ snapshots: acorn@8.15.0: {} - adm-zip@0.6.0: {} - agent-base@7.1.4: {} ajv-formats@3.0.1(ajv@8.17.1): @@ -9701,8 +9273,6 @@ snapshots: transitivePeerDependencies: - supports-color - boolean@3.2.0: {} - brace-expansion@1.1.18: dependencies: balanced-match: 1.0.2 @@ -10126,20 +9696,8 @@ snapshots: bundle-name: 4.1.0 default-browser-id: 5.0.1 - define-data-property@1.1.4: - dependencies: - es-define-property: 1.0.1 - es-errors: 1.3.0 - gopd: 1.2.0 - define-lazy-prop@3.0.0: {} - define-properties@1.2.1: - dependencies: - define-data-property: 1.1.4 - has-property-descriptors: 1.0.2 - object-keys: 1.1.1 - delaunator@5.0.1: dependencies: robust-predicates: 3.0.2 @@ -10152,8 +9710,6 @@ snapshots: detect-node-es@1.1.0: {} - detect-node@2.1.0: {} - devlop@1.1.0: dependencies: dequal: 2.0.3 @@ -10254,8 +9810,6 @@ snapshots: es-toolkit@1.50.0: {} - es6-error@4.1.1: {} - esbuild@0.27.2: optionalDependencies: '@esbuild/aix-ppc64': 0.27.2 @@ -10554,8 +10108,6 @@ snapshots: flatted: 3.4.3 keyv: 4.5.4 - flatbuffers@25.9.23: {} - flatted@3.4.3: {} formatly@0.3.0: @@ -10649,34 +10201,18 @@ snapshots: dependencies: is-glob: 4.0.3 - global-agent@3.0.0: - dependencies: - boolean: 3.2.0 - es6-error: 4.1.1 - matcher: 3.0.0 - roarr: 2.15.4 - semver: 7.8.5 - serialize-error: 7.0.1 - global-directory@4.0.1: dependencies: ini: 4.1.1 globals@14.0.0: {} - globalthis@1.0.4: - dependencies: - define-properties: 1.2.1 - gopd: 1.2.0 - gopd@1.2.0: {} graceful-fs@4.2.11: {} graphql@16.12.0: {} - guid-typescript@1.0.9: {} - hachure-fill@0.5.2: {} happy-dom@20.8.9: @@ -10693,10 +10229,6 @@ snapshots: has-flag@4.0.0: {} - has-property-descriptors@1.0.2: - dependencies: - es-define-property: 1.0.1 - has-symbols@1.1.0: {} hasown@2.0.2: @@ -10968,8 +10500,6 @@ snapshots: json-stable-stringify-without-jsonify@1.0.1: {} - json-stringify-safe@5.0.1: {} - json5@2.2.3: {} jsonfile@6.2.0: @@ -11156,8 +10686,6 @@ snapshots: chalk: 5.6.2 is-unicode-supported: 1.3.0 - long@5.3.2: {} - longest-streak@3.1.0: {} loose-envify@1.4.0: @@ -11199,10 +10727,6 @@ snapshots: marked@16.4.2: {} - matcher@3.0.0: - dependencies: - escape-string-regexp: 4.0.0 - math-intrinsics@1.1.0: {} mdast-util-find-and-replace@3.0.2: @@ -11691,8 +11215,6 @@ snapshots: object-inspect@1.13.4: {} - object-keys@1.1.1: {} - object-treeify@1.1.33: {} obug@2.1.3: {} @@ -11721,25 +11243,6 @@ snapshots: regex: 6.1.0 regex-recursion: 6.0.2 - onnxruntime-common@1.24.0-dev.20251116-b39e144322: {} - - onnxruntime-common@1.24.3: {} - - onnxruntime-node@1.24.3: - dependencies: - adm-zip: 0.6.0 - global-agent: 3.0.0 - onnxruntime-common: 1.24.3 - - onnxruntime-web@1.26.0-dev.20260416-b7804b056c: - dependencies: - flatbuffers: 25.9.23 - guid-typescript: 1.0.9 - long: 5.3.2 - onnxruntime-common: 1.24.0-dev.20251116-b39e144322 - platform: 1.3.6 - protobufjs: 7.6.5 - open@11.0.0: dependencies: default-browser: 5.4.0 @@ -11890,8 +11393,6 @@ snapshots: mlly: 1.8.0 pathe: 2.0.3 - platform@1.3.6: {} - playwright-core@1.61.1: {} playwright-core@1.62.0-alpha-1783623505000: {} @@ -12058,20 +11559,6 @@ snapshots: prosemirror-state: 1.4.4 prosemirror-transform: 1.11.0 - protobufjs@7.6.5: - dependencies: - '@protobufjs/aspromise': 1.1.2 - '@protobufjs/base64': 1.1.2 - '@protobufjs/codegen': 2.0.5 - '@protobufjs/eventemitter': 1.1.1 - '@protobufjs/fetch': 1.1.1 - '@protobufjs/float': 1.0.2 - '@protobufjs/path': 1.1.2 - '@protobufjs/pool': 1.1.0 - '@protobufjs/utf8': 1.1.1 - '@types/node': 20.19.28 - long: 5.3.2 - proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 @@ -12382,15 +11869,6 @@ snapshots: reusify@1.1.0: {} - roarr@2.15.4: - dependencies: - boolean: 3.2.0 - detect-node: 2.1.0 - globalthis: 1.0.4 - json-stringify-safe: 5.0.1 - semver-compare: 1.0.0 - sprintf-js: 1.1.3 - robust-predicates@3.0.2: {} rolldown@1.0.3: @@ -12451,8 +11929,6 @@ snapshots: refa: 0.12.1 regexp-ast-analysis: 0.7.1 - semver-compare@1.0.0: {} - semver@6.3.1: {} semver@7.7.4: {} @@ -12475,10 +11951,6 @@ snapshots: transitivePeerDependencies: - supports-color - serialize-error@7.0.1: - dependencies: - type-fest: 0.13.1 - serve-static@2.2.1: dependencies: encodeurl: 2.0.0 @@ -12533,39 +12005,6 @@ snapshots: - supports-color - typescript - sharp@0.35.3(@types/node@20.19.28): - dependencies: - '@img/colour': 1.1.0 - detect-libc: 2.1.2 - semver: 7.8.5 - optionalDependencies: - '@img/sharp-darwin-arm64': 0.35.3 - '@img/sharp-darwin-x64': 0.35.3 - '@img/sharp-freebsd-wasm32': 0.35.3 - '@img/sharp-libvips-darwin-arm64': 1.3.2 - '@img/sharp-libvips-darwin-x64': 1.3.2 - '@img/sharp-libvips-linux-arm': 1.3.2 - '@img/sharp-libvips-linux-arm64': 1.3.2 - '@img/sharp-libvips-linux-ppc64': 1.3.2 - '@img/sharp-libvips-linux-riscv64': 1.3.2 - '@img/sharp-libvips-linux-s390x': 1.3.2 - '@img/sharp-libvips-linux-x64': 1.3.2 - '@img/sharp-libvips-linuxmusl-arm64': 1.3.2 - '@img/sharp-libvips-linuxmusl-x64': 1.3.2 - '@img/sharp-linux-arm': 0.35.3 - '@img/sharp-linux-arm64': 0.35.3 - '@img/sharp-linux-ppc64': 0.35.3 - '@img/sharp-linux-riscv64': 0.35.3 - '@img/sharp-linux-s390x': 0.35.3 - '@img/sharp-linux-x64': 0.35.3 - '@img/sharp-linuxmusl-arm64': 0.35.3 - '@img/sharp-linuxmusl-x64': 0.35.3 - '@img/sharp-webcontainers-wasm32': 0.35.3 - '@img/sharp-win32-arm64': 0.35.3 - '@img/sharp-win32-ia32': 0.35.3 - '@img/sharp-win32-x64': 0.35.3 - '@types/node': 20.19.28 - shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -12634,8 +12073,6 @@ snapshots: split2@4.2.0: {} - sprintf-js@1.1.3: {} - stackback@0.0.2: {} state-local@1.0.7: {} @@ -12790,8 +12227,6 @@ snapshots: dependencies: prelude-ls: 1.2.1 - type-fest@0.13.1: {} - type-fest@5.4.0: dependencies: tagged-tag: 1.0.0 diff --git a/apps/web/components/app-sidebar/sections/settings/settings-tree-render.test.tsx b/apps/web/components/app-sidebar/sections/settings/settings-tree-render.test.tsx index 9782ed42fe..6a5ef6a99e 100644 --- a/apps/web/components/app-sidebar/sections/settings/settings-tree-render.test.tsx +++ b/apps/web/components/app-sidebar/sections/settings/settings-tree-render.test.tsx @@ -9,7 +9,10 @@ const MAIN_WORKSPACE_NAME = "Main Workspace"; // name plus that badge — match on the name rather than the whole string. const WORKSPACE_ROW = new RegExp(MAIN_WORKSPACE_NAME); const SEARCH_LABEL = "Search settings"; -const VOICE_MODE_LABEL = "Voice Mode"; +// A section that lives inside Task Behavior rather than owning a nav row — +// the same shape Voice Mode had before it moved to a plugin. Used as the probe +// for "sections never render as page rows". +const NESTED_SECTION_LABEL = "Message Queue"; const WORKSPACES_ROW_KEY = "row:/settings/workspaces"; const AGENTS_ROW_KEY = "row:/settings/agents"; const WORKSPACE_KEY = `workspace:${MAIN_WORKSPACE_ID}`; @@ -192,10 +195,10 @@ describe("SettingsTree static menu", () => { expect(screen.queryByRole("link", { name: "API Tokens" })).toBeNull(); }); - it("keeps Voice Mode inside Task Behavior instead of as a page row", () => { + it("keeps a section inside Task Behavior instead of rendering it as a page row", () => { render(); - expect(screen.queryByRole("link", { name: VOICE_MODE_LABEL })).toBeNull(); + expect(screen.queryByRole("link", { name: NESTED_SECTION_LABEL })).toBeNull(); expect(screen.getByRole("link", { name: "Task Behavior" }).getAttribute("data-active")).toBe( "true", ); @@ -516,7 +519,7 @@ describe("SettingsTree search", () => { render(); const search = screen.getByRole("searchbox", { name: SEARCH_LABEL }); - expect(screen.queryByRole("link", { name: VOICE_MODE_LABEL })).toBeNull(); + expect(screen.queryByRole("link", { name: NESTED_SECTION_LABEL })).toBeNull(); fireEvent.change(search, { target: { value: "font size" } }); @@ -525,7 +528,7 @@ describe("SettingsTree search", () => { "/settings/preferences/terminal-editors#setting-terminal-font-size", ); expect(result.textContent).toContain("Terminal & Editors"); - expect(screen.queryByRole("link", { name: VOICE_MODE_LABEL })).toBeNull(); + expect(screen.queryByRole("link", { name: NESTED_SECTION_LABEL })).toBeNull(); }); it("prefixes per-workspace results with the workspace name", () => { @@ -550,7 +553,7 @@ describe("SettingsTree search", () => { fireEvent.keyDown(search, { key: "Escape" }); expect((search as HTMLInputElement).value).toBe(""); - expect(screen.queryByRole("link", { name: VOICE_MODE_LABEL })).toBeNull(); + expect(screen.queryByRole("link", { name: NESTED_SECTION_LABEL })).toBeNull(); }); it("announces an empty result without rendering the normal menu", () => { @@ -561,6 +564,6 @@ describe("SettingsTree search", () => { }); expect(screen.getByText("No matching settings")).toBeTruthy(); - expect(screen.queryByRole("link", { name: "Voice Mode" })).toBeNull(); + expect(screen.queryByRole("link", { name: NESTED_SECTION_LABEL })).toBeNull(); }); }); diff --git a/apps/web/components/settings/external-mcp-settings.tsx b/apps/web/components/settings/external-mcp-settings.tsx index 3f79a7610a..980acd5a80 100644 --- a/apps/web/components/settings/external-mcp-settings.tsx +++ b/apps/web/components/settings/external-mcp-settings.tsx @@ -73,7 +73,7 @@ export function ExternalMcpSettings() { // `localhost` is a hostname the user types, not copy. Interpolated so // the pseudo-locale leaves it intact — baked into the message it renders // as `ĺōćàĺĥōśţ`, a dead pointer to something the reader must reproduce - // verbatim. Same reasoning as `voiceUnavailableInsecure`, which already + // verbatim. The scheme and host are passed as values so a translator // passes its scheme and host as values. description={t("settings:externalMcpEndpointsDescription", { localhostHost: "localhost", diff --git a/apps/web/components/settings/keyboard-shortcuts-card.tsx b/apps/web/components/settings/keyboard-shortcuts-card.tsx index 5e15a4ae74..89c4b5ff53 100644 --- a/apps/web/components/settings/keyboard-shortcuts-card.tsx +++ b/apps/web/components/settings/keyboard-shortcuts-card.tsx @@ -35,7 +35,7 @@ type ShortcutRecorderProps = { current: KeyboardShortcut; onChange: (id: string, shortcut: KeyboardShortcut) => void; onReset: (id: string) => void; - // Optional: callers that don't support an explicit "unbind" (e.g. the voice + // Optional: callers that don't support an explicit "unbind" (e.g. a single-row // settings recorder) omit this, and the Clear button is hidden for them. onClear?: (id: string) => void; isDirty?: boolean; diff --git a/apps/web/components/settings/task-behavior-settings.tsx b/apps/web/components/settings/task-behavior-settings.tsx index 27776e34fe..f05ab8f29d 100644 --- a/apps/web/components/settings/task-behavior-settings.tsx +++ b/apps/web/components/settings/task-behavior-settings.tsx @@ -7,7 +7,6 @@ import { TaskActionsSettings } from "@/components/settings/general-settings"; import { SettingsSection } from "@/components/settings/settings-section"; import { SettingsTarget } from "@/components/settings/settings-target"; import { MessageQueueSettings } from "@/components/settings/system/message-queue-settings"; -import { VoiceModeSettings } from "@/components/settings/voice-mode-settings"; import { GENERAL_SETTINGS_TARGETS } from "@/lib/settings-discovery/catalog/preferences"; /** Task behavior: the former Task Actions and Message Queue pages as one page. */ @@ -27,7 +26,6 @@ export function TaskBehaviorSettings() { - ); } diff --git a/apps/web/components/settings/voice-mode-settings.test.tsx b/apps/web/components/settings/voice-mode-settings.test.tsx deleted file mode 100644 index 19e2e59ce6..0000000000 --- a/apps/web/components/settings/voice-mode-settings.test.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import { act, cleanup, fireEvent, render, screen } from "@testing-library/react"; -import type { ReactNode } from "react"; -import { afterEach, describe, expect, it, vi } from "vitest"; -import type { SettingsSaveContributor } from "./settings-save-provider"; -import { VoiceModeSettings } from "./voice-mode-settings"; - -const updateUserSettingsMock = vi.fn(); -const setUserSettingsMock = vi.fn(); -const MIC_BUTTON_LABEL = "Show the mic button on the chat composer"; -let saveContributor: SettingsSaveContributor | null = null; -const initialVoiceMode = { - enabled: true, - engine: "auto" as const, - language: "auto", - mode: "toggle" as const, - autoSend: false, - whisperWebModel: "base" as const, -}; -const state = { - userSettings: { - voiceMode: initialVoiceMode, - keyboardShortcuts: {}, - }, - setUserSettings: setUserSettingsMock, -}; - -vi.mock("@kandev/ui/kbd", () => ({ - Kbd: ({ children }: { children: ReactNode }) => {children}, -})); - -vi.mock("@/components/state-provider", () => ({ - useAppStore: (selector: (value: typeof state) => unknown) => selector(state), - useAppStoreApi: () => ({ getState: () => state }), -})); - -vi.mock("@/lib/api", () => ({ - updateUserSettings: (...args: unknown[]) => updateUserSettingsMock(...args), -})); - -vi.mock("./settings-save-provider", () => ({ - useSettingsSaveContributor: (contributor: SettingsSaveContributor) => { - saveContributor = contributor; - }, -})); - -afterEach(() => { - cleanup(); - updateUserSettingsMock.mockReset(); - setUserSettingsMock.mockReset(); - state.userSettings = { voiceMode: initialVoiceMode, keyboardShortcuts: {} }; - saveContributor = null; -}); - -describe("VoiceModeSettings", () => { - it("stages voice configuration until the route save runs", async () => { - updateUserSettingsMock.mockResolvedValue(undefined); - render(); - - fireEvent.click(screen.getByRole("switch", { name: MIC_BUTTON_LABEL })); - - expect(updateUserSettingsMock).not.toHaveBeenCalled(); - expect(saveContributor?.isDirty).toBe(true); - expect( - screen.getByRole("switch", { name: MIC_BUTTON_LABEL }).getAttribute("data-settings-dirty"), - ).toBe("true"); - expect(screen.getByTestId("voice-enable-card").getAttribute("data-settings-dirty")).toBe( - "true", - ); - - await saveContributor?.save(saveContributor.revision); - - expect(updateUserSettingsMock).toHaveBeenCalledWith( - expect.objectContaining({ voice_mode: expect.objectContaining({ enabled: false }) }), - ); - }); - - it("rebases unchanged shortcuts before saving a voice draft", async () => { - updateUserSettingsMock.mockResolvedValue(undefined); - render(); - - fireEvent.click(screen.getByRole("switch", { name: MIC_BUTTON_LABEL })); - state.userSettings = { - ...state.userSettings, - keyboardShortcuts: { voice_toggle: { key: "v" } }, - }; - if (!saveContributor) throw new Error("Save contributor was not registered"); - - await act(async () => saveContributor?.save(saveContributor.revision)); - - expect(updateUserSettingsMock).toHaveBeenCalledWith( - expect.objectContaining({ keyboard_shortcuts: { voice_toggle: { key: "v" } } }), - ); - expect(saveContributor?.isDirty).toBe(false); - }); - - it("preserves a voice edit made while a save is in flight", async () => { - let resolveSave: () => void = () => undefined; - updateUserSettingsMock.mockReturnValue( - new Promise((resolve) => { - resolveSave = resolve; - }), - ); - render(); - const toggle = screen.getByRole("switch", { - name: MIC_BUTTON_LABEL, - }); - fireEvent.click(toggle); - if (!saveContributor) throw new Error("Save contributor was not registered"); - let savePromise!: Promise; - - act(() => { - savePromise = Promise.resolve(saveContributor?.save(saveContributor.revision)); - }); - fireEvent.click(toggle); - await act(async () => { - resolveSave(); - await savePromise; - }); - - expect(toggle.getAttribute("aria-checked")).toBe("true"); - expect(saveContributor?.isDirty).toBe(true); - }); -}); diff --git a/apps/web/components/settings/voice-mode-settings.tsx b/apps/web/components/settings/voice-mode-settings.tsx deleted file mode 100644 index ad09f50017..0000000000 --- a/apps/web/components/settings/voice-mode-settings.tsx +++ /dev/null @@ -1,626 +0,0 @@ -"use client"; - -import { createContext, useCallback, useContext, useMemo, useState, type ReactNode } from "react"; -import { useTranslation } from "react-i18next"; -import { IconAlertTriangle, IconMicrophone } from "@tabler/icons-react"; -import { Badge } from "@kandev/ui/badge"; -import { CardContent, CardHeader, CardTitle } from "@kandev/ui/card"; -import { Label } from "@kandev/ui/label"; -import { RadioGroup, RadioGroupItem } from "@kandev/ui/radio-group"; -import { - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectLabel, - SelectTrigger, - SelectValue, -} from "@kandev/ui/select"; -import { Switch } from "@kandev/ui/switch"; -import { useAppStore, useAppStoreApi } from "@/components/state-provider"; -import { updateUserSettings } from "@/lib/api"; -import { SettingsSection } from "@/components/settings/settings-section"; -import { ShortcutRecorder } from "@/components/settings/keyboard-shortcuts-card"; -import { detectVoiceCapabilities, type VoiceCapabilities } from "@/lib/voice/capabilities"; -import type { UserSettingsState, VoiceModeState } from "@/lib/state/slices/settings/types"; -import type { KeyboardShortcut } from "@/lib/keyboard/constants"; -import { - CONFIGURABLE_SHORTCUTS, - getShortcut, - type StoredShortcutOverrides, -} from "@/lib/keyboard/shortcut-overrides"; -import type { - VoiceInputActivationMode, - VoiceInputEngine, - VoiceModeSettings as VoiceModeWire, - WhisperWebModelSize, -} from "@/lib/types/http-voice"; -import { useSettingsSaveContributor } from "./settings-save-provider"; -import { SettingsCard } from "./settings-card"; -import { GENERAL_SETTINGS_TARGETS } from "@/lib/settings-discovery/catalog/preferences"; - -const VOICE_SECURE_SCHEME = "HTTPS"; -const VOICE_LOCALHOST_HOST = "localhost"; -const VOICE_INSECURE_SCHEME = "HTTP"; -const VOICE_LOCALHOST_URL = "http://localhost"; - -// Single source of truth for the language options. Web Speech reads `lang`, -// Whisper engines treat it as a hint. "auto" defers to the browser locale. -// `value` is the BCP-47 tag handed to the engine, so it is never translated; -// the labels travel as catalog keys and resolve at render. -const LANGUAGE_OPTIONS: Array<{ value: string; labelKey: string }> = [ - { value: "auto", labelKey: "settings:voiceLanguageAuto" }, - { value: "en-US", labelKey: "settings:voiceLanguageEnUs" }, - { value: "en-GB", labelKey: "settings:voiceLanguageEnGb" }, - { value: "es-ES", labelKey: "settings:voiceLanguageEsEs" }, - { value: "es-MX", labelKey: "settings:voiceLanguageEsMx" }, - { value: "pt-PT", labelKey: "settings:voiceLanguagePtPt" }, - { value: "pt-BR", labelKey: "settings:voiceLanguagePtBr" }, - { value: "fr-FR", labelKey: "settings:voiceLanguageFrFr" }, - { value: "de-DE", labelKey: "settings:voiceLanguageDeDe" }, - { value: "it-IT", labelKey: "settings:voiceLanguageItIt" }, - { value: "ja-JP", labelKey: "settings:voiceLanguageJaJp" }, - { value: "zh-CN", labelKey: "settings:voiceLanguageZhCn" }, -]; - -// `value` is the persisted `WhisperWebModelSize` enum and `size` is a download -// size in binary units, neither of which is prose. Label and hint are copy. -const WHISPER_MODELS: Array<{ - value: WhisperWebModelSize; - labelKey: string; - size: string; - hintKey: string; -}> = [ - { - value: "tiny", - labelKey: "settings:voiceWhisperTiny", - size: "~40 MB", - hintKey: "settings:voiceWhisperTinyHint", - }, - { - value: "base", - labelKey: "settings:voiceWhisperBase", - size: "~75 MB", - hintKey: "settings:voiceWhisperBaseHint", - }, - { - value: "small", - labelKey: "settings:voiceWhisperSmall", - size: "~240 MB", - hintKey: "settings:voiceWhisperSmallHint", - }, -]; - -function toWire(state: VoiceModeState): VoiceModeWire { - return { - enabled: state.enabled, - engine: state.engine, - language: state.language, - mode: state.mode, - auto_send: state.autoSend, - whisper_web_model: state.whisperWebModel, - }; -} - -type VoiceDraft = { - voiceMode: VoiceModeState; - keyboardShortcuts: StoredShortcutOverrides; -}; - -type VoiceDraftContextValue = VoiceDraft & { - savedVoiceMode: VoiceModeState; - savedKeyboardShortcuts: StoredShortcutOverrides; - updateVoiceMode: (patch: Partial) => void; - updateShortcuts: (shortcuts: StoredShortcutOverrides) => void; -}; - -const VoiceDraftContext = createContext(null); - -function voiceDraftFromSettings(settings: UserSettingsState): VoiceDraft { - return { - voiceMode: settings.voiceMode, - keyboardShortcuts: settings.keyboardShortcuts, - }; -} - -function useVoiceDraft() { - const value = useContext(VoiceDraftContext); - if (!value) throw new Error("Voice settings require VoiceDraftProvider"); - return value; -} - -function VoiceDraftProvider({ children }: { children: ReactNode }) { - const userSettings = useAppStore((state) => state.userSettings); - const setUserSettings = useAppStore((state) => state.setUserSettings); - const storeApi = useAppStoreApi(); - const currentSettingsDraft = voiceDraftFromSettings(userSettings); - const [saved, setSaved] = useState(currentSettingsDraft); - const [draft, setDraft] = useState(saved); - const revision = JSON.stringify(draft); - const savedRevision = JSON.stringify(saved); - const currentSettingsRevision = JSON.stringify(currentSettingsDraft); - - if (revision === savedRevision && currentSettingsRevision !== savedRevision) { - setSaved(currentSettingsDraft); - setDraft(currentSettingsDraft); - } - - useSettingsSaveContributor({ - id: "voice-mode", - revision, - isDirty: revision !== savedRevision, - save: async () => { - const latest = voiceDraftFromSettings(storeApi.getState().userSettings); - const submitted = { - voiceMode: - JSON.stringify(draft.voiceMode) === JSON.stringify(saved.voiceMode) - ? latest.voiceMode - : draft.voiceMode, - keyboardShortcuts: - JSON.stringify(draft.keyboardShortcuts) === JSON.stringify(saved.keyboardShortcuts) - ? latest.keyboardShortcuts - : draft.keyboardShortcuts, - }; - await updateUserSettings({ - voice_mode: toWire(submitted.voiceMode), - keyboard_shortcuts: submitted.keyboardShortcuts, - }); - setSaved(submitted); - setDraft((current) => ({ - voiceMode: - JSON.stringify(current.voiceMode) === JSON.stringify(draft.voiceMode) - ? submitted.voiceMode - : current.voiceMode, - keyboardShortcuts: - JSON.stringify(current.keyboardShortcuts) === JSON.stringify(draft.keyboardShortcuts) - ? submitted.keyboardShortcuts - : current.keyboardShortcuts, - })); - setUserSettings({ ...storeApi.getState().userSettings, ...submitted }); - }, - discard: () => setDraft(saved), - }); - - const value = useMemo( - () => ({ - ...draft, - savedVoiceMode: saved.voiceMode, - savedKeyboardShortcuts: saved.keyboardShortcuts, - updateVoiceMode: (patch) => - setDraft((current) => ({ - ...current, - voiceMode: { ...current.voiceMode, ...patch }, - })), - updateShortcuts: (keyboardShortcuts) => - setDraft((current) => ({ ...current, keyboardShortcuts })), - }), - [draft, saved], - ); - - return {children}; -} - -// ── Draft hook ─────────────────────────────────────────────────────────── - -function useVoiceModeSaver() { - const { updateVoiceMode } = useVoiceDraft(); - return { save: updateVoiceMode, saving: false }; -} - -// ── Engine card ────────────────────────────────────────────────────────── - -// `value` is the persisted `VoiceInputEngine` enum; everything else is copy and -// travels as a catalog key, because this builder holds no JSX and is therefore -// invisible to the literal guard. -type EngineOption = { - value: VoiceInputEngine; - labelKey: string; - descriptionKey: string; - badgeKey?: string; - disabled?: boolean; -}; - -const ENGINE_UNSUPPORTED_KEY = "settings:voiceEngineUnsupported"; - -function buildEngineOptions(caps: VoiceCapabilities): EngineOption[] { - return [ - { - value: "auto", - labelKey: "settings:voiceEngineAuto", - descriptionKey: "settings:voiceEngineAutoDescription", - }, - { - value: "webSpeech", - labelKey: "settings:voiceEngineWebSpeech", - descriptionKey: caps.webSpeech - ? "settings:voiceEngineWebSpeechDescription" - : ENGINE_UNSUPPORTED_KEY, - disabled: !caps.webSpeech, - }, - { - value: "whisperWeb", - labelKey: "settings:voiceEngineWhisperWeb", - descriptionKey: caps.whisperWeb - ? "settings:voiceEngineWhisperWebDescription" - : ENGINE_UNSUPPORTED_KEY, - badgeKey: "settings:voiceEngineBadgeLocal", - disabled: !caps.whisperWeb, - }, - { - value: "whisperServer", - labelKey: "settings:voiceEngineWhisperServer", - descriptionKey: caps.audioCapture - ? "settings:voiceEngineWhisperServerDescription" - : ENGINE_UNSUPPORTED_KEY, - badgeKey: "settings:voiceEngineBadgeServer", - disabled: !caps.audioCapture, - }, - ]; -} - -function EngineCard({ caps }: { caps: VoiceCapabilities }) { - const { t } = useTranslation(); - const { voiceMode, savedVoiceMode } = useVoiceDraft(); - const { save, saving } = useVoiceModeSaver(); - const options = useMemo(() => buildEngineOptions(caps), [caps]); - - return ( - - - {t("settings:voiceEngineTitle")} - - - save({ engine: v as VoiceInputEngine })} - disabled={saving} - className="space-y-3" - > - {options.map((opt) => ( - - ))} - - - - ); -} - -// ── Behavior card (language + mode + auto-send) ────────────────────────── - -function LanguageRow() { - const { t } = useTranslation(); - const { voiceMode, savedVoiceMode } = useVoiceDraft(); - const { save, saving } = useVoiceModeSaver(); - return ( -
- - -

{t("settings:voiceLanguageHint")}

-
- ); -} - -function ModeRow() { - const { t } = useTranslation(); - const { voiceMode, savedVoiceMode } = useVoiceDraft(); - const { save, saving } = useVoiceModeSaver(); - return ( -
- - save({ mode: v as VoiceInputActivationMode })} - disabled={saving} - className="flex gap-4" - data-settings-dirty={voiceMode.mode !== savedVoiceMode.mode} - > - - - -
- ); -} - -function AutoSendRow() { - const { t } = useTranslation(); - const { voiceMode, savedVoiceMode } = useVoiceDraft(); - const { save, saving } = useVoiceModeSaver(); - return ( -
-
- -

{t("settings:voiceAutoSendDescription")}

-
- save({ autoSend: checked })} - disabled={saving} - data-settings-dirty={voiceMode.autoSend !== savedVoiceMode.autoSend} - /> -
- ); -} - -function BehaviorCard() { - const { t } = useTranslation(); - const { voiceMode, savedVoiceMode } = useVoiceDraft(); - const isDirty = - voiceMode.language !== savedVoiceMode.language || - voiceMode.mode !== savedVoiceMode.mode || - voiceMode.autoSend !== savedVoiceMode.autoSend; - return ( - - - {t("settings:voiceBehavior")} - - - - - - - - ); -} - -// ── Whisper Web model card ─────────────────────────────────────────────── - -function WhisperModelCard() { - const { t } = useTranslation(); - const { voiceMode, savedVoiceMode } = useVoiceDraft(); - const { save, saving } = useVoiceModeSaver(); - - return ( - - - {t("settings:voiceWhisperModelTitle")} - - - save({ whisperWebModel: v as WhisperWebModelSize })} - disabled={saving} - className="space-y-2" - > - {WHISPER_MODELS.map((m) => ( - - ))} - -

{t("settings:voiceWhisperModelHint")}

-
-
- ); -} - -// ── Enable card (top-level on/off) ─────────────────────────────────────── - -function EnableCard() { - const { t } = useTranslation(); - const { voiceMode, savedVoiceMode } = useVoiceDraft(); - const { save, saving } = useVoiceModeSaver(); - return ( - - - {t("settings:voiceEnableTitle")} - - -
-
- -

{t("settings:voiceEnableDescription")}

-
- save({ enabled: checked })} - disabled={saving} - data-settings-dirty={voiceMode.enabled !== savedVoiceMode.enabled} - /> -
-
-
- ); -} - -// ── Availability banner ────────────────────────────────────────────────── - -function AvailabilityBanner({ caps }: { caps: VoiceCapabilities }) { - const { t } = useTranslation(); - if (caps.webSpeech || caps.whisperWeb || caps.audioCapture) return null; - // Secure-context requirement is the most common reason capability detection - // returns all-false on mobile (when reaching the dev server over LAN HTTP). - // Spell it out so the user doesn't have to guess. - const insecure = typeof window !== "undefined" && !window.isSecureContext; - return ( -
- -
-

{t("settings:voiceUnavailableTitle")}

-

- {insecure - ? t("settings:voiceUnavailableInsecure", { - secureScheme: VOICE_SECURE_SCHEME, - localhostHost: VOICE_LOCALHOST_HOST, - insecureScheme: VOICE_INSECURE_SCHEME, - localhostUrl: VOICE_LOCALHOST_URL, - }) - : t("settings:voiceUnavailableUnsupported")} -

-
-
- ); -} - -// ── Voice keyboard shortcut card ───────────────────────────────────────── - -function useShortcutSaver() { - return useVoiceDraft().updateShortcuts; -} - -function VoiceShortcutCard() { - const { t } = useTranslation(); - const { keyboardShortcuts: overrides, savedKeyboardShortcuts } = useVoiceDraft(); - const persist = useShortcutSaver(); - const current = getShortcut("VOICE_INPUT_TOGGLE", overrides); - const savedCurrent = getShortcut("VOICE_INPUT_TOGGLE", savedKeyboardShortcuts); - const isDirty = JSON.stringify(current) !== JSON.stringify(savedCurrent); - - const handleChange = useCallback( - (_id: string, shortcut: KeyboardShortcut) => - persist({ ...overrides, VOICE_INPUT_TOGGLE: shortcut }), - [overrides, persist], - ); - const handleReset = useCallback(() => { - const next = { ...overrides }; - delete next.VOICE_INPUT_TOGGLE; - persist(next); - }, [overrides, persist]); - - return ( - - - - {/* The shortcut's own label comes from the shared keyboard registry, - which is still English — see the guard comment for this route. */} - {t("settings:voiceShortcutTitle", { - shortcut: CONFIGURABLE_SHORTCUTS.VOICE_INPUT_TOGGLE.label, - })} - - - - -

{t("settings:voiceShortcutHint")}

-
-
- ); -} - -// ── Page ───────────────────────────────────────────────────────────────── - -function VoiceModeSettingsContent() { - const { t } = useTranslation(); - const caps = useMemo(() => detectVoiceCapabilities(), []); - const { voiceMode } = useVoiceDraft(); - const enabled = voiceMode.enabled; - return ( - } - title={t("settings:voiceMode")} - description={t("settings:voiceModePageDescription")} - discoveryTargetId={GENERAL_SETTINGS_TARGETS.voiceMode} - > -
- - {/* When voice is disabled, keep showing the secondary cards but dim - them — preserves the visible configuration without implying it has - any effect right now. */} -
-
- - - - - -
-
-
-
- ); -} - -export function VoiceModeSettings() { - return ( - - - - ); -} diff --git a/apps/web/components/task-create-dialog-form-body.test.tsx b/apps/web/components/task-create-dialog-form-body.test.tsx index 3bd0e3ea96..b54b21450d 100644 --- a/apps/web/components/task-create-dialog-form-body.test.tsx +++ b/apps/web/components/task-create-dialog-form-body.test.tsx @@ -215,9 +215,9 @@ describe("DialogPromptSection (CLI-mode parity)", () => { expect((last.linearImport as { disabled: boolean } | undefined)?.disabled).toBe(false); }); - it("forwards onVoiceAutoSend to TaskFormInputs", () => { + it("forwards onComposerSubmit to TaskFormInputs", () => { taskFormInputsCalls.length = 0; - const onVoiceAutoSend = () => true; + const onComposerSubmit = () => true; render( { initialDescription="" fs={makeFs()} handleKeyDown={(() => {}) as never} - onVoiceAutoSend={onVoiceAutoSend} + onComposerSubmit={onComposerSubmit} />, ); const last = taskFormInputsCalls.at(-1)!; - expect(last.onVoiceAutoSend).toBe(onVoiceAutoSend); + expect(last.onComposerSubmit).toBe(onComposerSubmit); }); }); diff --git a/apps/web/components/task-create-dialog-form-body.tsx b/apps/web/components/task-create-dialog-form-body.tsx index b6732381f0..00f58df0de 100644 --- a/apps/web/components/task-create-dialog-form-body.tsx +++ b/apps/web/components/task-create-dialog-form-body.tsx @@ -376,8 +376,8 @@ export type DialogPromptSectionProps = { * description should pass `false` so the name field wins focus. */ autoFocusDescription?: boolean; - /** Called after a non-empty voice transcript is inserted and auto-send is on. */ - onVoiceAutoSend?: () => boolean | Promise; + /** Submits the form for a plugin composer action that finished producing text. */ + onComposerSubmit?: () => boolean | Promise; }; // importBindings collapses the optional Jira/Linear import callbacks into the @@ -408,7 +408,7 @@ export function DialogPromptSection({ descriptionPlaceholder, aboveDescriptionSlot, autoFocusDescription, - onVoiceAutoSend, + onComposerSubmit, }: DialogPromptSectionProps) { const importsEnabled = !isSessionMode && !isTaskStarted; const ws = workspaceId ?? null; @@ -433,7 +433,7 @@ export function DialogPromptSection({ isUtilityConfigured={enhance?.isConfigured} jiraImport={importBindings(importsEnabled, ws, onJiraImport)} linearImport={importBindings(importsEnabled, ws, onLinearImport)} - onVoiceAutoSend={onVoiceAutoSend} + onComposerSubmit={onComposerSubmit} /> { return { ...actual, processFile: vi.fn() }; }); -// Capture the props (notably `onTranscript` / `onAutoSend`) that -// TaskFormInputs hands the voice button so we can drive transcripts -// without instantiating the real VoiceInputButton (which subscribes to -// the user-settings store and instantiates voice engines). -type VoiceProps = { - onTranscript: (text: string) => void; - onAutoSend?: () => void; - disabled?: boolean; -}; -const voiceCalls: VoiceProps[] = []; +// Captures the slot props TaskFormInputs hands its plugin composer action, +// which is the only way text now reaches the description programmatically. const pluginSlotCalls: PluginComposerSlotProps[] = []; -vi.mock("@/components/task/chat/voice-input-button", () => ({ - VoiceInputButton: (props: VoiceProps) => { - voiceCalls.push(props); - return , })); -vi.mock("./voice-input-button", () => ({ - VoiceInputButton: () => , -})); - // Records what the slot was handed instead of rendering plugin components. vi.mock("./chat-input-plugin-actions", () => ({ ChatInputPluginActions: (props: Record) => { diff --git a/apps/web/components/task/chat/chat-input-toolbar-desktop.tsx b/apps/web/components/task/chat/chat-input-toolbar-desktop.tsx index e5ed030357..683c3b0831 100644 --- a/apps/web/components/task/chat/chat-input-toolbar-desktop.tsx +++ b/apps/web/components/task/chat/chat-input-toolbar-desktop.tsx @@ -14,7 +14,6 @@ import { cn } from "@/lib/utils"; import { ResetContextButton } from "./reset-context-button"; import { ImplementPlanButton } from "./implement-plan-button"; import { ChatInputPluginActions } from "./chat-input-plugin-actions"; -import { VoiceInputButton } from "./voice-input-button"; import { ContextPopover } from "./context-popover"; import { AttachFilesButton, @@ -159,8 +158,6 @@ function DesktopRightSection(props: { onCancel: () => void | Promise; onSubmit: () => void; submitShortcut: (typeof SHORTCUTS)[keyof typeof SHORTCUTS]; - onVoiceTranscript?: (text: string) => void; - onVoiceAutoSend?: () => void; composerCapability?: ChatInputToolbarProps["composerCapability"]; composerSurface?: ChatInputToolbarProps["composerSurface"]; }) { @@ -187,13 +184,6 @@ function DesktopRightSection(props: { /> )}
- {props.onVoiceTranscript && ( - - )} diff --git a/apps/web/components/task/chat/chat-input-toolbar-mobile.tsx b/apps/web/components/task/chat/chat-input-toolbar-mobile.tsx index 4e63bf6c86..dd87c94492 100644 --- a/apps/web/components/task/chat/chat-input-toolbar-mobile.tsx +++ b/apps/web/components/task/chat/chat-input-toolbar-mobile.tsx @@ -7,7 +7,6 @@ import { ModeSelector } from "@/components/task/mode-selector"; import { SessionsDropdown } from "@/components/task/sessions-dropdown"; import { TokenUsageDisplay } from "@/components/task/chat/token-usage-display"; import { EnhancePromptButton } from "@/components/enhance-prompt-button"; -import { VoiceInputButton } from "./voice-input-button"; import { ChatInputPluginActions } from "./chat-input-plugin-actions"; import { ContextPopover } from "./context-popover"; import { ImplementPlanButton } from "./implement-plan-button"; @@ -56,8 +55,6 @@ type MobileToolbarProps = { onCancel: () => void | Promise; onSubmit: () => void; submitShortcut: (typeof SHORTCUTS)[keyof typeof SHORTCUTS]; - onVoiceTranscript?: (text: string) => void; - onVoiceAutoSend?: () => void; composerCapability?: PluginComposerCapability; composerSurface?: "task-chat" | "quick-chat"; }; @@ -188,13 +185,6 @@ export function MobileChatInputToolbar(props: MobileToolbarProps) { composer={props.composerCapability} /> )} - {props.onVoiceTranscript && ( - - )} ({ ChatInputPluginActions: () => null, })); -vi.mock("./voice-input-button", () => ({ - VoiceInputButton: () => , -})); - import { ChatInputToolbar } from "./chat-input-toolbar"; import type { ChatInputToolbarProps } from "./chat-input-toolbar"; diff --git a/apps/web/components/task/chat/chat-input-toolbar.tsx b/apps/web/components/task/chat/chat-input-toolbar.tsx index 8444e989bf..2aa5a00806 100644 --- a/apps/web/components/task/chat/chat-input-toolbar.tsx +++ b/apps/web/components/task/chat/chat-input-toolbar.tsx @@ -48,12 +48,6 @@ export type ChatInputToolbarProps = { isUtilityConfigured?: boolean; /** Callback to open file picker for attaching files */ onAttachFiles?: () => void; - /** Callback to insert a transcribed voice utterance into the editor. When - * omitted, the voice button is hidden — keeps quick-chat / read-only - * variants free of a button they can't wire. */ - onVoiceTranscript?: (text: string) => void; - /** Optional auto-send hook fired after a voice transcript is inserted. */ - onVoiceAutoSend?: () => void; /** Hide the sessions dropdown (for quick chat) */ hideSessionsDropdown?: boolean; /** When true, only render the submit/cancel button — no other controls */ @@ -183,8 +177,6 @@ export const ChatInputToolbar = memo(function ChatInputToolbar(rawProps: ChatInp onCancel={props.onCancel} onSubmit={props.onSubmit} submitShortcut={submitShortcut} - onVoiceTranscript={props.onVoiceTranscript} - onVoiceAutoSend={props.onVoiceAutoSend} composerCapability={props.composerCapability} composerSurface={props.composerSurface} /> diff --git a/apps/web/components/task/chat/voice-input-button.test.tsx b/apps/web/components/task/chat/voice-input-button.test.tsx deleted file mode 100644 index e69c8a4620..0000000000 --- a/apps/web/components/task/chat/voice-input-button.test.tsx +++ /dev/null @@ -1,245 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { cleanup, fireEvent, render, screen } from "@testing-library/react"; - -// ── Hoisted mocks (must be set before importing the component under test) ── - -const voicePrefs = vi.hoisted(() => ({ - value: { - enabled: true, - engine: "auto" as "auto" | "webSpeech" | "whisperWeb" | "whisperServer", - language: "auto", - mode: "hold" as "hold" | "toggle", - autoSend: false, - whisperWebModel: "base" as "tiny" | "base" | "small", - }, -})); - -const voiceInputResult = vi.hoisted(() => ({ - supported: true as boolean, - state: "idle" as "idle" | "requesting" | "recording" | "processing", - modelLoad: { state: "idle" as const, progress: 0 }, - start: vi.fn(async () => undefined), - stop: vi.fn(async () => undefined), - cancel: vi.fn(() => undefined), - engine: "webSpeech" as const, - error: null as null | { code: string; message: string }, -})); - -const coarsePointer = vi.hoisted(() => ({ value: false })); - -vi.mock("@/components/state-provider", () => ({ - useAppStore: ( - selector: (state: { - userSettings: { - voiceMode: typeof voicePrefs.value; - keyboardShortcuts: Record; - }; - }) => unknown, - ) => - selector({ - userSettings: { voiceMode: voicePrefs.value, keyboardShortcuts: {} }, - }), -})); - -vi.mock("@/hooks/use-voice-input", () => ({ - useVoiceInput: () => voiceInputResult, -})); - -vi.mock("@/hooks/use-keyboard-shortcut", () => ({ - useKeyboardShortcut: () => undefined, -})); - -vi.mock("@/components/toast-provider", () => ({ - useToast: () => ({ toast: vi.fn() }), -})); - -vi.mock("@kandev/ui/tooltip", () => ({ - Tooltip: ({ children }: { children: React.ReactNode }) => <>{children}, - TooltipTrigger: ({ children }: { children: React.ReactNode }) => <>{children}, - TooltipContent: ({ children }: { children: React.ReactNode }) => <>{children}, -})); - -// ── matchMedia stub honouring the hoisted `coarsePointer.value` flag ── - -beforeEach(() => { - voicePrefs.value = { - enabled: true, - engine: "auto", - language: "auto", - mode: "hold", - autoSend: false, - whisperWebModel: "base", - }; - voiceInputResult.supported = true; - voiceInputResult.state = "idle"; - voiceInputResult.modelLoad = { state: "idle", progress: 0 }; - voiceInputResult.start.mockClear(); - voiceInputResult.stop.mockClear(); - voiceInputResult.cancel.mockClear(); - coarsePointer.value = false; - - Object.defineProperty(window, "matchMedia", { - configurable: true, - writable: true, - value: (query: string) => ({ - matches: query.includes("coarse") ? coarsePointer.value : false, - media: query, - onchange: null, - addEventListener: () => undefined, - removeEventListener: () => undefined, - addListener: () => undefined, - removeListener: () => undefined, - dispatchEvent: () => false, - }), - }); -}); - -afterEach(() => { - cleanup(); -}); - -// Re-import after the mocks so the module under test sees the mocked deps. -import { VoiceInputButton } from "./voice-input-button"; - -const BUTTON_TESTID = "voice-input-button"; - -function renderButton() { - return render( {}} onAutoSend={() => {}} />); -} - -describe("VoiceInputButton — hold-mode pointer handlers", () => { - it("captures the pointer on pointerdown and starts recording", () => { - renderButton(); - const button = screen.getByTestId(BUTTON_TESTID); - const setCap = vi.fn(); - (button as unknown as { setPointerCapture: (id: number) => void }).setPointerCapture = setCap; - - fireEvent.pointerDown(button, { pointerId: 7 }); - - expect(setCap).toHaveBeenCalledWith(7); - expect(voiceInputResult.start).toHaveBeenCalledTimes(1); - }); - - it("releases the pointer and stops on pointerup", () => { - renderButton(); - const button = screen.getByTestId(BUTTON_TESTID); - const releaseCap = vi.fn(); - (button as unknown as { hasPointerCapture: () => boolean }).hasPointerCapture = () => true; - (button as unknown as { releasePointerCapture: (id: number) => void }).releasePointerCapture = - releaseCap; - - fireEvent.pointerUp(button, { pointerId: 7 }); - - expect(releaseCap).toHaveBeenCalledWith(7); - expect(voiceInputResult.stop).toHaveBeenCalledTimes(1); - }); - - it("stops on pointercancel without triggering on pointerleave", () => { - renderButton(); - const button = screen.getByTestId(BUTTON_TESTID); - (button as unknown as { hasPointerCapture: () => boolean }).hasPointerCapture = () => false; - (button as unknown as { releasePointerCapture: (id: number) => void }).releasePointerCapture = - () => undefined; - - // pointerleave used to abort recording. Confirm it no longer does. - fireEvent.pointerLeave(button, { pointerId: 7 }); - expect(voiceInputResult.stop).not.toHaveBeenCalled(); - - fireEvent.pointerCancel(button, { pointerId: 7 }); - expect(voiceInputResult.stop).toHaveBeenCalledTimes(1); - }); - - it("does not throw if setPointerCapture is not present on the element", () => { - renderButton(); - const button = screen.getByTestId(BUTTON_TESTID); - // Some test/UA combinations strip setPointerCapture — handler must swallow. - expect(() => fireEvent.pointerDown(button, { pointerId: 1 })).not.toThrow(); - }); -}); - -describe("VoiceInputButton — coarse-pointer override", () => { - it("reports stored hold mode but effective toggle when matchMedia(coarse) matches", () => { - coarsePointer.value = true; - renderButton(); - const button = screen.getByTestId(BUTTON_TESTID); - expect(button.getAttribute("data-mode")).toBe("hold"); - expect(button.getAttribute("data-effective-mode")).toBe("toggle"); - }); - - it("does not attach hold pointer handlers in coarse-pointer toggle fallback", () => { - coarsePointer.value = true; - renderButton(); - const button = screen.getByTestId(BUTTON_TESTID); - fireEvent.pointerDown(button, { pointerId: 1 }); - fireEvent.pointerUp(button, { pointerId: 1 }); - // toggle handler is bound to onClick; pointer events alone should be inert. - expect(voiceInputResult.start).not.toHaveBeenCalled(); - expect(voiceInputResult.stop).not.toHaveBeenCalled(); - - // Click is what drives toggle. - fireEvent.click(button); - expect(voiceInputResult.start).toHaveBeenCalledTimes(1); - }); - - it("preserves hold behaviour on fine-pointer devices", () => { - coarsePointer.value = false; - renderButton(); - const button = screen.getByTestId(BUTTON_TESTID); - expect(button.getAttribute("data-effective-mode")).toBe("hold"); - }); - - it("applies the 40px touch-target class on coarse-pointer devices", () => { - coarsePointer.value = true; - renderButton(); - const button = screen.getByTestId(BUTTON_TESTID); - expect(button.className).toContain("h-10"); - expect(button.className).toContain("w-10"); - expect(button.className).not.toContain("h-7"); - }); -}); - -describe("VoiceInputButton — stored toggle mode is unaffected by pointer kind", () => { - it("renders effective toggle and ignores pointer events when stored mode is toggle", () => { - voicePrefs.value = { ...voicePrefs.value, mode: "toggle" }; - coarsePointer.value = false; - renderButton(); - const button = screen.getByTestId(BUTTON_TESTID); - expect(button.getAttribute("data-mode")).toBe("toggle"); - expect(button.getAttribute("data-effective-mode")).toBe("toggle"); - - // Pointer handlers must not be attached in toggle mode; pointerdown alone - // should not start a recording. Click is what drives toggle. - fireEvent.pointerDown(button, { pointerId: 4 }); - expect(voiceInputResult.start).not.toHaveBeenCalled(); - fireEvent.click(button); - expect(voiceInputResult.start).toHaveBeenCalledTimes(1); - }); -}); - -describe("VoiceInputButton — recording-state stop transition", () => { - it("invokes stop() on click when state is recording (toggle path)", () => { - voicePrefs.value = { ...voicePrefs.value, mode: "toggle" }; - voiceInputResult.state = "recording"; - renderButton(); - const button = screen.getByTestId(BUTTON_TESTID); - fireEvent.click(button); - expect(voiceInputResult.stop).toHaveBeenCalledTimes(1); - expect(voiceInputResult.start).not.toHaveBeenCalled(); - }); -}); - -describe("VoiceInputButton — disabled feature & unsupported fallback", () => { - it("renders nothing when voice mode is disabled in settings", () => { - voicePrefs.value = { ...voicePrefs.value, enabled: false }; - const { container } = renderButton(); - expect(container.firstChild).toBeNull(); - }); - - it("renders the unsupported fallback button when no engine is available", () => { - voiceInputResult.supported = false; - renderButton(); - const button = screen.getByTestId(BUTTON_TESTID); - expect(button.getAttribute("data-state")).toBe("unsupported"); - expect(button.getAttribute("aria-label")).toBe("Voice input unavailable"); - }); -}); diff --git a/apps/web/components/task/chat/voice-input-button.tsx b/apps/web/components/task/chat/voice-input-button.tsx deleted file mode 100644 index b9b85f0d6b..0000000000 --- a/apps/web/components/task/chat/voice-input-button.tsx +++ /dev/null @@ -1,425 +0,0 @@ -"use client"; - -import { useCallback, useEffect, useRef, useSyncExternalStore } from "react"; -import { IconLoader2, IconMicrophone, IconPlayerStopFilled } from "@tabler/icons-react"; - -import { Button } from "@kandev/ui/button"; -import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip"; -import { cn } from "@/lib/utils"; -import { - useVoiceInput, - type VoiceError, - type VoiceInputState, - type VoiceModelLoadState, -} from "@/hooks/use-voice-input"; -import { useAppStore } from "@/components/state-provider"; -import { useKeyboardShortcut } from "@/hooks/use-keyboard-shortcut"; -import { useToast } from "@/components/toast-provider"; -import { getShortcut } from "@/lib/keyboard/shortcut-overrides"; -import { whisperModelConfig } from "@/lib/voice/whisper-web-models"; -import { VoiceModelLoadIndicator } from "./voice-model-load-indicator"; -import { useTranslation } from "react-i18next"; -import type { TFunction } from "i18next"; - -type VoiceInputButtonProps = { - /** Inserts the recognized transcript at the current cursor position. */ - onTranscript: (text: string) => void; - /** Called after a non-empty transcript was inserted, when auto-send is enabled. */ - onAutoSend?: () => void; - /** Disable while the chat input itself is disabled (sending / starting / failed). */ - disabled?: boolean; -}; - -// Catalog keys, not copy: both tables are built at module load, where a `t()` -// call would freeze at the boot locale. They resolve at render. -const TOOLTIP_KEY_BY_STATE: Record = { - idle: "task:voiceInput", - requesting: "task:requestingMicrophone", - recording: "task:stopRecording", - processing: "task:transcribing", -}; - -const ARIA_KEY_BY_STATE: Record = { - idle: "task:startVoiceInput", - requesting: "task:requestingMicrophonePermission", - recording: "task:stopVoiceInput", - processing: "task:transcribingVoiceInput", -}; - -function ButtonIcon({ - state, - modelLoad, -}: { - state: VoiceInputState; - modelLoad: VoiceModelLoadState; -}) { - if (state === "processing" || state === "requesting" || modelLoad.state === "loading") { - return ; - } - if (state === "recording") { - return ; - } - return ; -} - -function toastForError(toast: ReturnType["toast"], err: VoiceError) { - if (err.code === "no-speech") { - toast({ title: err.message }); - return; - } - toast({ title: err.message, variant: "error" }); -} - -// ── Activation handlers ────────────────────────────────────────────────── - -// Hold mode survives finger drift by claiming the pointer with -// setPointerCapture on pointerdown. Without capture, any movement that crosses -// the button's bounds (a common occurrence on touch — small finger shift, -// soft-keyboard reflow, OS gesture handoff) fires pointerleave and would stop -// recording mid-utterance. pointerleave is intentionally NOT wired here for -// the same reason. Capture is released on pointerup/pointercancel; both also -// trigger stop(). -function safePointerCapture(target: Element, pointerId: number): void { - try { - target.setPointerCapture(pointerId); - } catch { - // setPointerCapture can throw InvalidPointerId on browsers that have - // already released the implicit capture (older WebKit). Safe to ignore — - // the worst case is the prior behaviour where hold could stop on drift. - } -} - -function safePointerRelease(target: Element, pointerId: number): void { - try { - // `hasPointerCapture` is in lib.dom but the runtime check guards against - // ancient/headless environments that strip it (some jsdom + happy-dom - // versions). If the method is present and reports no capture, skip the - // release to avoid the no-op throw Safari occasionally fires. - if (typeof target.hasPointerCapture === "function" && !target.hasPointerCapture(pointerId)) { - return; - } - target.releasePointerCapture(pointerId); - } catch { - // ignore — Safari can throw if capture was already released. - } -} - -function buildHoldHandlers(start: () => Promise, stop: () => Promise) { - return { - onPointerDown: (e: React.PointerEvent) => { - e.preventDefault(); - safePointerCapture(e.currentTarget, e.pointerId); - void start(); - }, - onPointerUp: (e: React.PointerEvent) => { - e.preventDefault(); - safePointerRelease(e.currentTarget, e.pointerId); - void stop(); - }, - onPointerCancel: (e: React.PointerEvent) => { - safePointerRelease(e.currentTarget, e.pointerId); - void stop(); - }, - }; -} - -function buildToggleHandler( - state: VoiceInputState, - start: () => Promise, - stop: () => Promise, -) { - return () => { - if (state === "idle") void start(); - else if (state === "recording") void stop(); - }; -} - -// ── Hook composition ───────────────────────────────────────────────────── - -function useAutoSendOnTranscript( - baseOnTranscript: (text: string) => void, - onAutoSend: (() => void) | undefined, - enabled: boolean, -) { - // Wrap onTranscript so we can defer auto-send until after the transcript - // has been inserted. requestAnimationFrame keeps a clean separation between - // the editor update and the submit handler, so the editor's onChange has - // already flushed when submit reads from it. - return useCallback( - (text: string) => { - baseOnTranscript(text); - if (enabled && onAutoSend) requestAnimationFrame(onAutoSend); - }, - [baseOnTranscript, onAutoSend, enabled], - ); -} - -// Subscribes to `(pointer: coarse)` so the component re-renders when the user -// docks/undocks an external pointer (Surface, iPad with Magic Keyboard). SSR -// returns false, matching desktop default — mobile-first hydration corrects on -// first client paint. Pattern mirrors `useResponsiveBreakpoint`. -function subscribeCoarsePointer(callback: () => void): () => void { - if (typeof window === "undefined" || typeof window.matchMedia !== "function") return () => {}; - const mql = window.matchMedia("(pointer: coarse)"); - mql.addEventListener("change", callback); - return () => mql.removeEventListener("change", callback); -} - -function getCoarsePointerSnapshot(): boolean { - if (typeof window === "undefined" || typeof window.matchMedia !== "function") return false; - return window.matchMedia("(pointer: coarse)").matches; -} - -function getCoarsePointerServerSnapshot(): boolean { - return false; -} - -function useIsCoarsePointer(): boolean { - return useSyncExternalStore( - subscribeCoarsePointer, - getCoarsePointerSnapshot, - getCoarsePointerServerSnapshot, - ); -} - -function useVoiceShortcut( - enabled: boolean, - state: VoiceInputState, - start: () => Promise, - stop: () => Promise, -) { - const overrides = useAppStore((s) => s.userSettings.keyboardShortcuts); - const shortcut = getShortcut("VOICE_INPUT_TOGGLE", overrides); - const stateRef = useRef(state); - useEffect(() => { - stateRef.current = state; - }, [state]); - const handler = useCallback(() => { - if (stateRef.current === "idle") void start(); - else if (stateRef.current === "recording") void stop(); - }, [start, stop]); - useKeyboardShortcut(shortcut, handler, { enabled }); -} - -// ── Unsupported fallback ──────────────────────────────────────────────── - -// URL schemes the reader has to reproduce verbatim in the address bar, not copy. -// Interpolated so the pseudo-locale and translators leave them intact — baked -// into the message, `https://` pseudolocalizes to `ĥţţƥś://`, a scheme that does -// not exist. Same reasoning as `settings:voiceUnavailableInsecure`, which -// already passes its scheme and host as values. -const VOICE_SECURE_SCHEME_URL = "https://"; -const VOICE_LOCALHOST_URL = "http://localhost"; - -function buildUnsupportedReasonKey(): string { - if (typeof window === "undefined") return "task:voiceInputUnavailableHere"; - if (!window.isSecureContext) return "task:voiceInputNeedsHttps"; - return "task:voiceInputUnsupportedBrowser"; -} - -function UnsupportedVoiceButton({ disabled }: { disabled?: boolean }) { - const { t } = useTranslation(); - const { toast } = useToast(); - const handleClick = () => { - toast({ - title: t("task:voiceInputUnavailable"), - // Only `voiceInputNeedsHttps` reads these; i18next ignores values a - // message does not reference, so they can be passed unconditionally. - description: t(buildUnsupportedReasonKey(), { - secureSchemeUrl: VOICE_SECURE_SCHEME_URL, - localhostUrl: VOICE_LOCALHOST_URL, - }), - variant: "error", - }); - }; - return ( - - - - - {t("task:voiceInputUnavailableTapForDetails")} - - ); -} - -// ── Component ──────────────────────────────────────────────────────────── - -export function VoiceInputButton({ onTranscript, onAutoSend, disabled }: VoiceInputButtonProps) { - const enabled = useAppStore((s) => s.userSettings.voiceMode.enabled); - // Render nothing — including no hook subscriptions — when the user has - // disabled the feature in settings. Distinct from `!supported` (browser - // limitation) which shows a tappable greyed icon. Done as a sub-component - // so the unconditional hook count stays the same in the active path. - if (!enabled) return null; - return ( - - ); -} - -// Hold-to-talk is unreliable on touch even with pointer capture: the platform -// reclaims the pointer for system gestures (back swipes, scroll-chains) and the -// user's stored preference becomes a trap. Coarse-pointer devices silently get -// toggle behaviour; persisted `voiceMode.mode` is left alone so docking a -// keyboard restores the user's choice without a save. -function resolveEffectiveMode( - prefMode: "hold" | "toggle", - isCoarsePointer: boolean, -): "hold" | "toggle" { - return prefMode === "hold" && !isCoarsePointer ? "hold" : "toggle"; -} - -function resolveTooltip(args: { - translate: TFunction; - modelLoad: VoiceModelLoadState; - modelLabel: string; - state: VoiceInputState; - holdMode: boolean; -}): string { - const { translate: t, modelLoad, modelLabel, state, holdMode } = args; - if (modelLoad.state === "loading") { - const pct = Number.isFinite(modelLoad.progress) - ? Math.min(100, Math.max(0, Math.round(modelLoad.progress * 100))) - : 0; - return t("task:downloadingModelPercent", { modelLabel, pct }); - } - const tooltip = t(TOOLTIP_KEY_BY_STATE[state]); - return holdMode && state === "idle" ? t("task:voiceTooltipHold", { tooltip }) : tooltip; -} - -type VoiceMicButtonProps = { - state: VoiceInputState; - modelLoad: VoiceModelLoadState; - disabled?: boolean; - storedMode: "hold" | "toggle"; - effectiveMode: "hold" | "toggle"; - isCoarsePointer: boolean; - pointerHandlers: ReturnType | Record; - onClick: (() => void) | undefined; -}; - -// Ghost styled to match the other toolbar actions (attach, enhance) — voice -// is one secondary input action among several, not a co-primary alongside -// Submit. Recording flips to a tinted destructive look + pulsing ring so the -// active state stays unmistakable without shouting from a primary fill. -// Touch-input sizing bumps to 10×10 (40px) for an easier finger target -// without disturbing the desktop layout. -function VoiceMicButton({ - state, - modelLoad, - disabled, - storedMode, - effectiveMode, - isCoarsePointer, - pointerHandlers, - onClick, -}: VoiceMicButtonProps) { - const { t } = useTranslation(); - const isRecording = state === "recording"; - const isBusy = state === "requesting" || state === "processing" || modelLoad.state === "loading"; - return ( - - ); -} - -function EnabledVoiceInputButton({ onTranscript, onAutoSend, disabled }: VoiceInputButtonProps) { - const { t } = useTranslation(); - const { toast } = useToast(); - const voiceMode = useAppStore((s) => s.userSettings.voiceMode); - const handleError = useCallback((err: VoiceError) => toastForError(toast, err), [toast]); - const wrappedTranscript = useAutoSendOnTranscript(onTranscript, onAutoSend, voiceMode.autoSend); - const isCoarsePointer = useIsCoarsePointer(); - - const { supported, state, modelLoad, start, stop, cancel } = useVoiceInput({ - onTranscript: wrappedTranscript, - onError: handleError, - }); - - // If the chat input gets disabled mid-recording, cancel rather than leave - // the mic indicator on. Hold-mode pointerup may not fire if focus moves. - useEffect(() => { - if (disabled && (state === "recording" || state === "requesting")) cancel(); - }, [disabled, state, cancel]); - - useVoiceShortcut(supported && !disabled, state, start, stop); - - // Always render the button — even when unsupported — so users can see it on - // mobile and tap to learn why voice input isn't working (usually a missing - // secure context, e.g. when reaching the dev server over LAN HTTP). Hiding - // the button silently left mobile users with no discoverable feedback. - if (!supported) return ; - - const effectiveMode = resolveEffectiveMode(voiceMode.mode, isCoarsePointer); - const holdMode = effectiveMode === "hold"; - const pointerHandlers = holdMode ? buildHoldHandlers(start, stop) : {}; - const onClick = holdMode ? undefined : buildToggleHandler(state, start, stop); - const modelLabel = whisperModelConfig(voiceMode.whisperWebModel).label; - const tooltipText = resolveTooltip({ translate: t, modelLoad, modelLabel, state, holdMode }); - - return ( -
- - - - - - {tooltipText} - -
- ); -} diff --git a/apps/web/components/task/chat/voice-model-load-indicator.test.tsx b/apps/web/components/task/chat/voice-model-load-indicator.test.tsx deleted file mode 100644 index 6744ebb7db..0000000000 --- a/apps/web/components/task/chat/voice-model-load-indicator.test.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { afterEach, describe, expect, it } from "vitest"; -import { cleanup, render, screen } from "@testing-library/react"; -import { VoiceModelLoadIndicator } from "./voice-model-load-indicator"; - -const MODEL_LABEL = "Whisper Base"; -const INDICATOR_TESTID = "voice-model-load-indicator"; - -function getIndicator() { - return screen.getByTestId(INDICATOR_TESTID); -} - -afterEach(() => { - cleanup(); -}); - -describe("VoiceModelLoadIndicator", () => { - it("renders nothing when state is idle", () => { - const { container } = render( - , - ); - expect(container.firstChild).toBeNull(); - }); - - it("renders nothing when state is ready", () => { - const { container } = render( - , - ); - expect(container.firstChild).toBeNull(); - }); - - it("renders progress bar and percent text when loading", () => { - render(); - const root = getIndicator(); - expect(root.getAttribute("data-state")).toBe("loading"); - expect(root.textContent).toContain("Downloading Whisper Base… 42%"); - expect(screen.getByRole("progressbar").getAttribute("aria-label")).toBe( - "Downloading Whisper Base, 42 percent", - ); - }); - - it("renders an error label when state is error", () => { - render(); - const root = getIndicator(); - expect(root.getAttribute("data-state")).toBe("error"); - expect(root.textContent).toContain(`${MODEL_LABEL} failed to load`); - expect(root.getAttribute("aria-label")).toBe(`${MODEL_LABEL} failed to load`); - }); - - it("clamps percent to 0–100 when progress is out of range", () => { - render(); - expect(getIndicator().textContent).toContain("100%"); - cleanup(); - - render(); - expect(getIndicator().textContent).toContain("0%"); - cleanup(); - - render( - , - ); - expect(getIndicator().textContent).toContain("0%"); - }); -}); diff --git a/apps/web/components/task/chat/voice-model-load-indicator.tsx b/apps/web/components/task/chat/voice-model-load-indicator.tsx deleted file mode 100644 index 2d0410bf00..0000000000 --- a/apps/web/components/task/chat/voice-model-load-indicator.tsx +++ /dev/null @@ -1,67 +0,0 @@ -"use client"; - -import { IconAlertTriangle } from "@tabler/icons-react"; -import { Progress } from "@kandev/ui/progress"; -import { useTranslation } from "react-i18next"; - -export type VoiceModelLoadIndicatorProps = { - state: "idle" | "loading" | "ready" | "error"; - /** 0–1 fraction; clamped before rendering. */ - progress: number; - modelLabel: string; -}; - -function clampPercent(progress: number): number { - if (!Number.isFinite(progress)) return 0; - const pct = Math.round(progress * 100); - if (pct < 0) return 0; - if (pct > 100) return 100; - return pct; -} - -export function VoiceModelLoadIndicator({ - state, - progress, - modelLabel, -}: VoiceModelLoadIndicatorProps) { - const { t } = useTranslation(); - if (state === "idle" || state === "ready") return null; - - const pct = clampPercent(progress); - - // Single stable role="status" wrapper so AT announces the loading→error - // transition (a newly-inserted live region would be silently ignored). - // (Radix) already renders its own role="progressbar" — the - // wrapper div intentionally has no role to avoid nested progressbar nodes. - return ( -
- {state === "error" ? ( - <> - - {t("task:failedToLoad", { modelLabel })} - - ) : ( -
- - {t("task:downloadingModelPercent", { modelLabel, pct })} - - -
- )} -
- ); -} diff --git a/apps/web/components/task/new-session-dialog.test.tsx b/apps/web/components/task/new-session-dialog.test.tsx index fa350459b7..33bdea8ad2 100644 --- a/apps/web/components/task/new-session-dialog.test.tsx +++ b/apps/web/components/task/new-session-dialog.test.tsx @@ -99,12 +99,12 @@ vi.mock("@/components/task-create-dialog-selectors", async () => { descriptionValueRef, initialDescription, onDescriptionChange, - onVoiceAutoSend, + onComposerSubmit, }: { descriptionValueRef: React.RefObject; initialDescription: string; onDescriptionChange: (hasContent: boolean) => void; - onVoiceAutoSend?: () => void; + onComposerSubmit?: () => void; }) { const valueRef = React.useRef(initialDescription); const [value, setValue] = React.useState(initialDescription); @@ -137,13 +137,15 @@ vi.mock("@/components/task-create-dialog-selectors", async () => { "button", { type: "button", - "aria-label": "Voice input", + // Stands in for a plugin composer action: insert text, then ask the + // dialog to submit the native way. + "aria-label": "Plugin composer action", onClick: () => { - updateValue("voice transcript"); - onVoiceAutoSend?.(); + updateValue("dictated prompt"); + onComposerSubmit?.(); }, }, - "Voice", + "Plugin action", ), ); } @@ -272,16 +274,16 @@ describe("NewSessionDialog", () => { ); }); - it("auto-sends a transcript inserted into a blank composer", async () => { + it("submits text a plugin composer action inserted into a blank composer", async () => { render(); - fireEvent.click(screen.getByRole("button", { name: "Voice input" })); + fireEvent.click(screen.getByRole("button", { name: "Plugin composer action" })); await waitFor(() => expect(mockLaunchSession).toHaveBeenCalledTimes(1)); expect(mockBuildStartRequest).toHaveBeenCalledWith( "task-1", "profile-1", - expect.objectContaining({ prompt: "voice transcript" }), + expect.objectContaining({ prompt: "dictated prompt" }), ); }); diff --git a/apps/web/components/task/new-session-dialog.tsx b/apps/web/components/task/new-session-dialog.tsx index 834e781027..f22603c7cb 100644 --- a/apps/web/components/task/new-session-dialog.tsx +++ b/apps/web/components/task/new-session-dialog.tsx @@ -32,7 +32,7 @@ import { Trans, useTranslation } from "react-i18next"; export type { HandoffPreset } from "./handoff-types"; -const VOICE_SUBMIT_EVENT = { preventDefault: () => {} } as unknown as FormEvent; +const PROGRAMMATIC_SUBMIT_EVENT = { preventDefault: () => {} } as unknown as FormEvent; type NewSessionDialogProps = { open: boolean; @@ -468,11 +468,11 @@ function NewSessionForm({ onEnhancePrompt={handleEnhancePrompt} isEnhancingPrompt={isEnhancingPrompt} isUtilityConfigured={isUtilityConfigured} - onVoiceAutoSend={() => { + onComposerSubmit={() => { if (isBusyState || hasPendingAttachmentUploads || !profileSelection.hasProfiles) { return false; } - void handleSubmit(VOICE_SUBMIT_EVENT); + void handleSubmit(PROGRAMMATIC_SUBMIT_EVENT); return true; }} /> diff --git a/apps/web/e2e/helpers/api-client.ts b/apps/web/e2e/helpers/api-client.ts index e9a6764f59..6a677d2c57 100644 --- a/apps/web/e2e/helpers/api-client.ts +++ b/apps/web/e2e/helpers/api-client.ts @@ -15,7 +15,6 @@ import type { TaskCIAutomationOptions, TaskCIAutomationPatch, } from "../../lib/types/github"; -import type { VoiceModeSettings } from "../../lib/types/http-voice"; import type { TaskStatusSummary } from "../../lib/types/task-status-summary"; import type { SecretListItem, SecretScope } from "../../lib/types/http-secrets"; import type { @@ -969,7 +968,6 @@ export class ApiClient { tasks_list_sort?: string; tasks_list_group?: string; task_create_last_used?: TaskCreateLastUsedApi; - voice_mode?: VoiceModeSettings; kanban_hidden_step_ids?: Record; }): Promise { await this.request("PATCH", "/api/v1/user/settings", settings); diff --git a/apps/web/e2e/tests/chat/mobile-voice-mode.spec.ts b/apps/web/e2e/tests/chat/mobile-voice-mode.spec.ts deleted file mode 100644 index cefc036746..0000000000 --- a/apps/web/e2e/tests/chat/mobile-voice-mode.spec.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { test, expect } from "../../fixtures/test-base"; -import type { SeedData } from "../../fixtures/test-base"; -import type { ApiClient } from "../../helpers/api-client"; -import { SessionPage } from "../../pages/session-page"; - -// Mobile (Pixel 5) regression coverage for the voice-mode entry point and -// the chat composer's mic button. Filename matches /mobile-.*\.spec\.ts/ so -// the `mobile-chrome` Playwright project picks it up. -// -// Two assertions: -// 1. The Task Behavior page renders the Voice Mode section and all cards at mobile width when -// reached through the settings index — guards against the "setting missing" symptom. -// 2. The mic button mounts on the mobile chat composer with the coarse-pointer -// override: a user with stored `voiceMode.mode = "hold"` gets toggle -// behaviour (data-effective-mode = "toggle") and a 40px touch target. - -async function seedTask(apiClient: ApiClient, seedData: SeedData, title: string) { - return apiClient.createTaskWithAgent(seedData.workspaceId, title, seedData.agentProfileId, { - description: "/e2e:simple-message", - workflow_id: seedData.workflowId, - workflow_step_id: seedData.startStepId, - repository_ids: [seedData.repositoryId], - }); -} - -test.describe("Mobile voice mode", () => { - test.describe.configure({ retries: 1, timeout: 60_000 }); - - test("Voice Mode settings section renders every card at mobile width", async ({ - testPage, - prCapture, - }) => { - await testPage.goto("/settings"); - const index = testPage.getByTestId("settings-index"); - await index.getByRole("link", { name: /^Task Behavior/ }).click(); - await expect(testPage).toHaveURL(/\/settings\/preferences\/task-behavior$/); - - // The merged page keeps the Voice Mode section heading visible at the - // same route as the other task behavior settings. - await expect(testPage.getByRole("heading", { name: "Voice Mode", exact: true })).toBeVisible({ - timeout: 15_000, - }); - - // The five cards rendered by `voice-mode-settings.tsx` at mobile width. - await expect(testPage.getByText("Enable Voice Input")).toBeVisible(); - await expect(testPage.getByText("Transcription Engine")).toBeVisible(); - // Exact: the settings menu's "Task Behavior" row is mounted (display-hidden) - // at this width, and a substring match would collide with it. - await expect(testPage.getByText("Behavior", { exact: true })).toBeVisible(); - await expect(testPage.getByText("Whisper Web Model")).toBeVisible(); - await expect(testPage.getByText(/Shortcut$/)).toBeVisible(); - expect(await testPage.evaluate(() => document.documentElement.scrollWidth)).toBe( - await testPage.evaluate(() => document.documentElement.clientWidth), - ); - await testPage - .getByRole("heading", { name: "Voice Mode", exact: true }) - .scrollIntoViewIfNeeded(); - await prCapture.screenshot("task-behavior-voice-mode-mobile", { - caption: "Mobile Task Behavior page with Voice Mode settings", - }); - // (The legacy `button[data-sidebar="trigger"]` mobile settings-sidebar - // trigger was removed by the unified-AppSidebar overhaul — settings nav is - // now the footer-gear takeover, and the global rail is hidden on mobile.) - }); - - test("voice input mic button mounts on the mobile chat composer with the coarse-pointer touch target and effective toggle mode", async ({ - testPage, - apiClient, - seedData, - }) => { - // Seed hold mode so the coarse-pointer override assertion is non-vacuous: - // data-mode="hold" (stored pref) vs data-effective-mode="toggle" (override) - // proves the coarse-pointer branch fired, not that the default was toggle. - await apiClient.saveUserSettings({ - voice_mode: { - enabled: true, - engine: "auto", - language: "auto", - mode: "hold", - auto_send: false, - whisper_web_model: "base", - }, - }); - - const task = await seedTask(apiClient, seedData, "Mobile voice button"); - await testPage.goto(`/t/${task.id}`); - const session = new SessionPage(testPage); - await session.waitForLoad(); - - const micButton = testPage.getByTestId("voice-input-button"); - await expect(micButton).toBeVisible({ timeout: 15_000 }); - - // Pixel 5 advertises `(pointer: coarse)`. Three signals that the coarse- - // pointer override fired: - // - data-mode="hold" (the stored user preference) - // - data-effective-mode="toggle" (the override: hold→toggle on coarse) - // - h-10 w-10 (40px touch target, only set when isCoarsePointer=true) - await expect(micButton).toHaveAttribute("data-mode", "hold"); - await expect(micButton).toHaveAttribute("data-effective-mode", "toggle"); - await expect(micButton).toHaveClass(/(^|\s)h-10(\s|$)/); - await expect(micButton).toHaveClass(/(^|\s)w-10(\s|$)/); - }); -}); diff --git a/apps/web/e2e/tests/chat/toolbar-overflow.spec.ts b/apps/web/e2e/tests/chat/toolbar-overflow.spec.ts index add3595599..cafba0ad14 100644 --- a/apps/web/e2e/tests/chat/toolbar-overflow.spec.ts +++ b/apps/web/e2e/tests/chat/toolbar-overflow.spec.ts @@ -99,7 +99,7 @@ test.describe("Toolbar overflow menu", () => { await expect(contextBadge).not.toBeVisible(); // Submit button should remain visible (always-visible item). Target the - // submit testid specifically — the voice input button is also round, so a + // submit testid specifically — other toolbar buttons are round too, so a // bare `button.rounded-full` locator now matches both and fails strict mode. const submitBtn = toolbar.getByTestId("submit-message-button"); await expect(submitBtn).toBeVisible(); diff --git a/apps/web/e2e/tests/i18n/pseudo-coverage.spec.ts b/apps/web/e2e/tests/i18n/pseudo-coverage.spec.ts index be0685824a..f4790cff9d 100644 --- a/apps/web/e2e/tests/i18n/pseudo-coverage.spec.ts +++ b/apps/web/e2e/tests/i18n/pseudo-coverage.spec.ts @@ -238,8 +238,7 @@ const SCREENS: Screen[] = [ // translation — the same rule the eslint guard's keyboard pattern encodes. // // Shortcut names come from CONFIGURABLE_SHORTCUTS in - // lib/keyboard/shortcut-overrides.ts, a registry shared with the - // un-migrated voice-mode settings page; it migrates with that page. + // lib/keyboard/shortcut-overrides.ts. allow: [ "Ctrl", "Shift", @@ -262,7 +261,6 @@ const SCREENS: Screen[] = [ "Toggle Plan Mode", "Recent Task Switcher", "Recent Task Switcher (Backward)", - "Voice Input", "Reverse Chat Search", "Open Task Pull Request", "Open Workspace Picker", @@ -272,10 +270,6 @@ const SCREENS: Screen[] = [ name: "settings — task behavior", url: "/settings/preferences/task-behavior", anchor: "[data-testid=archive-confirmation-card]", - // Voice Mode reuses the shared keyboard shortcut registry. Its persisted - // shortcut label and rendered key chord are intentionally ASCII values, - // not translatable Task Behavior copy. - allow: ["Voice Input", "Ctrl+Shift+M"], }, { name: "settings — plugins", @@ -419,7 +413,7 @@ const SCREENS: Screen[] = [ // The settings-nav half of this blocker is now GONE: `settings-tree.tsx`, // `workspaces-group.tsx`, `executors-group.tsx`, `account-group.tsx` and // `theme-toggle.tsx` are migrated, so "Workspaces", "Integrations", - // "Automations", "Executors", "Voice Mode", "Utility Agents", "External MCP", + // "Automations", "Executors", "Utility Agents", "External MCP", // "Plugins" and "Toggle theme" render accented on every screen. Only "System" // is left, from `sections/settings/system-group.tsx`, which the System-routes // migration owns. @@ -435,7 +429,7 @@ const SCREENS: Screen[] = [ // `components/integrations/settings-section.tsx` chrome and `@kandev/ui`'s // built-in dialog "Close" label. // - // NOT YET: "settings — external mcp", "… prompts", "… voice mode", + // NOT YET: "settings — external mcp", "… prompts", // "… utility agents". All four pages' own copy is fully migrated and was // verified by running this oracle against each of them — every string those // routes own renders accented, including the 43 in the MCP tool catalog diff --git a/apps/web/e2e/tests/settings/settings-index.spec.ts b/apps/web/e2e/tests/settings/settings-index.spec.ts index 6d8c5ead81..b7d47d1996 100644 --- a/apps/web/e2e/tests/settings/settings-index.spec.ts +++ b/apps/web/e2e/tests/settings/settings-index.spec.ts @@ -65,28 +65,6 @@ test.describe("Settings index on desktop", () => { await expect(active).toHaveAccessibleName(/Appearance/); }); - test("owns Voice Mode inside Task Behavior", async ({ testPage, prCapture }) => { - await testPage.goto("/settings/preferences/task-behavior"); - - const settingsNav = testPage.getByTestId("app-sidebar-settings-mode"); - await expect( - settingsNav.getByRole("link", { name: "Task Behavior", exact: true }), - ).toBeVisible(); - await expect(settingsNav.getByRole("link", { name: "Voice Mode", exact: true })).toHaveCount(0); - await expect(testPage.getByRole("heading", { name: "Voice Mode", exact: true })).toBeVisible(); - await expect(testPage.getByTestId("voice-enable-card")).toBeVisible(); - await expect(testPage.getByText("Transcription Engine", { exact: true })).toBeVisible(); - await expect(testPage.getByText("Behavior", { exact: true })).toBeVisible(); - await expect(testPage.getByText("Whisper Web Model", { exact: true })).toBeVisible(); - await expect(testPage.getByText(/Shortcut$/, { exact: true })).toBeVisible(); - await testPage - .getByRole("heading", { name: "Voice Mode", exact: true }) - .scrollIntoViewIfNeeded(); - await prCapture.screenshot("task-behavior-voice-mode-desktop", { - caption: "Desktop Task Behavior page with Voice Mode settings", - }); - }); - test("redirects the /settings/general prefix to the Appearance page", async ({ testPage }) => { await testPage.goto("/settings/general"); diff --git a/apps/web/e2e/tests/settings/utility-agents.spec.ts b/apps/web/e2e/tests/settings/utility-agents.spec.ts index 1e21a6b8f4..bbd7c20cfa 100644 --- a/apps/web/e2e/tests/settings/utility-agents.spec.ts +++ b/apps/web/e2e/tests/settings/utility-agents.spec.ts @@ -360,11 +360,9 @@ test.describe("Utility Agents settings page", () => { ).toBeVisible(); }); - test("wraps each settings sub-section in a bounded card, matching Voice Mode", async ({ - testPage, - }) => { + test("wraps each settings sub-section in a bounded card", async ({ testPage }) => { // Regression guard: these sub-sections used to be bare divs with no - // visible border, unlike every other settings page (e.g. Voice Mode's + // visible border, unlike every other settings page (whose // Card-wrapped Enable/Engine/Behavior sections). // // Mock the inference-agent + builtin-agent APIs so the Actions card diff --git a/apps/web/e2e/tests/system/message-queue-settings.spec.ts b/apps/web/e2e/tests/system/message-queue-settings.spec.ts index 299ed07218..d5416ebd72 100644 --- a/apps/web/e2e/tests/system/message-queue-settings.spec.ts +++ b/apps/web/e2e/tests/system/message-queue-settings.spec.ts @@ -57,7 +57,9 @@ test.describe.serial("Message Queue general settings", () => { // The Message Queue box spans the full settings column like every other // settings box; it must not be width-restricted. const queueCard = testPage.getByTestId("message-queue-settings"); - const siblingCard = testPage.getByTestId("voice-enable-card"); + // Any other unconditional card on this page works as the reference width; + // this used to be the Voice Mode card, which moved out to the Voice plugin. + const siblingCard = testPage.getByTestId("archive-confirmation-card"); await expect(queueCard).toBeVisible(); await expect(siblingCard).toBeVisible(); expect((await queueCard.boundingBox())?.width ?? 0).toBeCloseTo( diff --git a/apps/web/eslint.i18n.options.mjs b/apps/web/eslint.i18n.options.mjs index cdee6c0a15..385c6456d5 100644 --- a/apps/web/eslint.i18n.options.mjs +++ b/apps/web/eslint.i18n.options.mjs @@ -265,9 +265,8 @@ export const i18nGuardFiles = [ // Settings → Preferences → Appearance, Keyboard Shortcuts and Task behavior. // The pages render from general-settings.tsx (already listed above); these // entries add the merged pages plus the per-setting cards those pages own. - // Shortcut *names* still come from `lib/keyboard/shortcut-overrides.ts`, a - // registry shared with the voice-mode settings section — deliberately not - // migrated here. + // Shortcut *names* still come from `lib/keyboard/shortcut-overrides.ts`, + // which is deliberately not migrated here. "components/settings/task-behavior-settings.tsx", "components/settings/terminal-editors-settings.tsx", "components/settings/anchored-prompt-bar-settings.tsx", @@ -968,11 +967,11 @@ export const i18nGuardFiles = [ "components/settings/repository-custom-scripts.tsx", "components/settings/repository-delete-dialog.tsx", "components/settings/unsaved-indicator.tsx", - // Settings → External MCP, Prompts, Voice Mode and Utility Agents, plus the - // two Changelog cards. Four small routes in one entry group because none of - // them owns enough copy to be worth its own migration, and they share the - // `settings` namespace: two of the four already had their page title there - // (`settings:voiceMode`, `settings:utilityAgents`) and the other two in + // Settings → External MCP, Prompts and Utility Agents, plus the two + // Changelog cards. Small routes in one entry group because none of them owns + // enough copy to be worth its own migration, and they share the `settings` + // namespace: one already had its page title there + // (`settings:utilityAgents`) and the others in // `common` (`common:externalMcp`, `common:prompts`), all of which are reused // rather than twinned — `SEGMENT_LABEL_KEYS` in `settings-layout-client.tsx` // renders the same words as the breadcrumb on these very routes. @@ -985,9 +984,8 @@ export const i18nGuardFiles = [ // identifiers an external agent calls. Two unit tests pin the keys to the // catalog, because nothing else can. // - // Three more shapes here held copy no lint rule could see, all now migrated: - // `buildEngineOptions` in `voice-mode-settings.tsx` (a plain builder - // returning label/description object literals), `noteForStatus` in + // Two more shapes here held copy no lint rule could see, both now migrated: + // `noteForStatus` in // `inference-agent-status.tsx` (a non-JSX switch over the probe status), and // the `placeholder = "Select a model"` parameter default in // `model-combobox.tsx`, which had to move into the component body — a @@ -1010,8 +1008,8 @@ export const i18nGuardFiles = [ // // - `app/settings/prompts/page.tsx` // are unreferenced. `SETTINGS_ROUTES` in `src/settings-routes.tsx` renders - // `` and `` directly, so the SSR - // prefetch in those pages never runs. `external-mcp` and `utility-agents` + // `` directly, so the SSR prefetch in that page never + // runs. `external-mcp` and `utility-agents` // go through their page, and are live. // - `app/settings/changelog/page.tsx` is likewise unreferenced; the route // table has its own `SettingsRedirect` to `/settings/system/updates`. @@ -1049,17 +1047,12 @@ export const i18nGuardFiles = [ // whole of every generated snippet. All interpolated as values. // - The `{{` prompt-template sigil and the `@name` chat mention token — // both typed verbatim by the user, both interpolated. - // - Wire values: the voice `engine` / `mode` / `whisper_web_model` unions - // and the BCP-47 `language` tags, the `InferenceAgentStatus` probe - // states, the `USE_DEFAULT` sentinel, and every agent/model id. Only - // their labels are copy, and those travel as catalog keys. - // - `~40 MB` / `~75 MB` / `~240 MB` in `WHISPER_MODELS`: download sizes in - // binary units, not prose — the same call `storage-units.ts` made. + // - Wire values: the `InferenceAgentStatus` probe states, the + // `USE_DEFAULT` sentinel, and every agent/model id. Only their labels + // are copy, and those travel as catalog keys. // - Utility agent `name`, `description` and `prompt`, and custom prompt // `name` / `content`. The builtins' text is authored by the backend and // the rest is user data; a prompt body is also sent to the agent verbatim. - // - `new Error("Voice settings require VoiceDraftProvider")`, a programming - // error that can only fire in a broken render tree. // // Every key added here was audited against all 15 en catalogs for English that // already exists under another key. Twelve matched. Ten are the established @@ -1088,12 +1081,12 @@ export const i18nGuardFiles = [ // that key is named for the secrets dialog it belongs to; folding the prompts // dialog into it would leave a key whose name contradicts half its callers. // - // Still English and NOT ours: `CONFIGURABLE_SHORTCUTS.VOICE_INPUT_TOGGLE.label` - // ("Voice Input") from `lib/keyboard/shortcut-overrides.ts`, interpolated into - // the migrated `settings:voiceShortcutTitle` frame. It is the shared keyboard - // registry, owned by whoever migrates `lib/keyboard`; under the pseudo-locale - // it reads as an English word inside accented copy, which is the oracle's - // known weak spot rather than a miss here. + // Shortcut labels in `CONFIGURABLE_SHORTCUTS` + // (`lib/keyboard/shortcut-overrides.ts`) are still English and NOT ours: + // that is the shared keyboard registry, owned by whoever migrates + // `lib/keyboard`. Under the pseudo-locale they read as English words inside + // accented copy, which is the oracle's known weak spot rather than a miss + // here. "app/settings/external-mcp/**/*.{ts,tsx}", "app/settings/prompts/**/*.{ts,tsx}", "app/settings/utility-agents/**/*.{ts,tsx}", @@ -1109,7 +1102,6 @@ export const i18nGuardFiles = [ "components/settings/utility-agents-section.tsx", "components/settings/utility-dirty.ts", "components/settings/utility-sections.tsx", - "components/settings/voice-mode-settings.tsx", "lib/settings/external-mcp-tools.ts", // Configuration Chat. `ConfigChatProvider` mounts the FAB on EVERY /settings // route, so its `aria-label="Configuration Chat"` was the last text finding diff --git a/apps/web/generated/licenses.json b/apps/web/generated/licenses.json index 70214d925d..66f8a93a3b 100644 --- a/apps/web/generated/licenses.json +++ b/apps/web/generated/licenses.json @@ -1329,30 +1329,6 @@ "license_text": "MIT License\n\nCopyright (c) 2022 - present, Yusuke Wada and Hono contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n", "ecosystem": "npm" }, - { - "name": "@huggingface/jinja", - "version": "0.5.9", - "license": "MIT", - "repository": "https://github.com/huggingface/huggingface.js.git", - "license_text": "MIT License\n\nCopyright (c) 2023 Hugging Face\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n", - "ecosystem": "npm" - }, - { - "name": "@huggingface/tokenizers", - "version": "0.1.3", - "license": "Apache-2.0", - "repository": "https://github.com/huggingface/tokenizers.js", - "license_text": " Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n", - "ecosystem": "npm" - }, - { - "name": "@huggingface/transformers", - "version": "4.2.0", - "license": "Apache-2.0", - "repository": "https://github.com/huggingface/transformers.js", - "license_text": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n", - "ecosystem": "npm" - }, { "name": "@iconify/types", "version": "2.0.0", @@ -1369,44 +1345,6 @@ "license_text": "MIT License\n\nCopyright (c) 2021-PRESENT Vjacheslav Trushkin\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.", "ecosystem": "npm" }, - { - "name": "@img/colour", - "version": "1.1.0", - "license": "MIT", - "repository": "https://github.com/lovell/colour", - "license_text": "# Licensing\n\n## color\n\nCopyright (c) 2012 Heather Arthur\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n## color-convert\n\nCopyright (c) 2011-2016 Heather Arthur .\nCopyright (c) 2016-2021 Josh Junon .\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n## color-string\n\nCopyright (c) 2011 Heather Arthur \n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n## color-name\n\nThe MIT License (MIT)\nCopyright (c) 2015 Dmitry Ivanov\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n", - "ecosystem": "npm" - }, - { - "name": "@img/sharp-libvips-linux-x64", - "version": "1.3.2", - "license": "LGPL-3.0-or-later", - "repository": "https://github.com/lovell/sharp-libvips", - "ecosystem": "npm" - }, - { - "name": "@img/sharp-libvips-linuxmusl-x64", - "version": "1.3.2", - "license": "LGPL-3.0-or-later", - "repository": "https://github.com/lovell/sharp-libvips", - "ecosystem": "npm" - }, - { - "name": "@img/sharp-linux-x64", - "version": "0.35.3", - "license": "Apache-2.0", - "repository": "https://github.com/lovell/sharp", - "license_text": "Apache License\nVersion 2.0, January 2004\nhttp://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n\"License\" shall mean the terms and conditions for use, reproduction, and\ndistribution as defined by Sections 1 through 9 of this document.\n\n\"Licensor\" shall mean the copyright owner or entity authorized by the copyright\nowner that is granting the License.\n\n\"Legal Entity\" shall mean the union of the acting entity and all other entities\nthat control, are controlled by, or are under common control with that entity.\nFor the purposes of this definition, \"control\" means (i) the power, direct or\nindirect, to cause the direction or management of such entity, whether by\ncontract or otherwise, or (ii) ownership of fifty percent (50%) or more of the\noutstanding shares, or (iii) beneficial ownership of such entity.\n\n\"You\" (or \"Your\") shall mean an individual or Legal Entity exercising\npermissions granted by this License.\n\n\"Source\" form shall mean the preferred form for making modifications, including\nbut not limited to software source code, documentation source, and configuration\nfiles.\n\n\"Object\" form shall mean any form resulting from mechanical transformation or\ntranslation of a Source form, including but not limited to compiled object code,\ngenerated documentation, and conversions to other media types.\n\n\"Work\" shall mean the work of authorship, whether in Source or Object form, made\navailable under the License, as indicated by a copyright notice that is included\nin or attached to the work (an example is provided in the Appendix below).\n\n\"Derivative Works\" shall mean any work, whether in Source or Object form, that\nis based on (or derived from) the Work and for which the editorial revisions,\nannotations, elaborations, or other modifications represent, as a whole, an\noriginal work of authorship. For the purposes of this License, Derivative Works\nshall not include works that remain separable from, or merely link (or bind by\nname) to the interfaces of, the Work and Derivative Works thereof.\n\n\"Contribution\" shall mean any work of authorship, including the original version\nof the Work and any modifications or additions to that Work or Derivative Works\nthereof, that is intentionally submitted to Licensor for inclusion in the Work\nby the copyright owner or by an individual or Legal Entity authorized to submit\non behalf of the copyright owner. For the purposes of this definition,\n\"submitted\" means any form of electronic, verbal, or written communication sent\nto the Licensor or its representatives, including but not limited to\ncommunication on electronic mailing lists, source code control systems, and\nissue tracking systems that are managed by, or on behalf of, the Licensor for\nthe purpose of discussing and improving the Work, but excluding communication\nthat is conspicuously marked or otherwise designated in writing by the copyright\nowner as \"Not a Contribution.\"\n\n\"Contributor\" shall mean Licensor and any individual or Legal Entity on behalf\nof whom a Contribution has been received by Licensor and subsequently\nincorporated within the Work.\n\n2. Grant of Copyright License.\n\nSubject to the terms and conditions of this License, each Contributor hereby\ngrants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,\nirrevocable copyright license to reproduce, prepare Derivative Works of,\npublicly display, publicly perform, sublicense, and distribute the Work and such\nDerivative Works in Source or Object form.\n\n3. Grant of Patent License.\n\nSubject to the terms and conditions of this License, each Contributor hereby\ngrants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,\nirrevocable (except as stated in this section) patent license to make, have\nmade, use, offer to sell, sell, import, and otherwise transfer the Work, where\nsuch license applies only to those patent claims licensable by such Contributor\nthat are necessarily infringed by their Contribution(s) alone or by combination\nof their Contribution(s) with the Work to which such Contribution(s) was\nsubmitted. If You institute patent litigation against any entity (including a\ncross-claim or counterclaim in a lawsuit) alleging that the Work or a\nContribution incorporated within the Work constitutes direct or contributory\npatent infringement, then any patent licenses granted to You under this License\nfor that Work shall terminate as of the date such litigation is filed.\n\n4. Redistribution.\n\nYou may reproduce and distribute copies of the Work or Derivative Works thereof\nin any medium, with or without modifications, and in Source or Object form,\nprovided that You meet the following conditions:\n\nYou must give any other recipients of the Work or Derivative Works a copy of\nthis License; and\nYou must cause any modified files to carry prominent notices stating that You\nchanged the files; and\nYou must retain, in the Source form of any Derivative Works that You distribute,\nall copyright, patent, trademark, and attribution notices from the Source form\nof the Work, excluding those notices that do not pertain to any part of the\nDerivative Works; and\nIf the Work includes a \"NOTICE\" text file as part of its distribution, then any\nDerivative Works that You distribute must include a readable copy of the\nattribution notices contained within such NOTICE file, excluding those notices\nthat do not pertain to any part of the Derivative Works, in at least one of the\nfollowing places: within a NOTICE text file distributed as part of the\nDerivative Works; within the Source form or documentation, if provided along\nwith the Derivative Works; or, within a display generated by the Derivative\nWorks, if and wherever such third-party notices normally appear. The contents of\nthe NOTICE file are for informational purposes only and do not modify the\nLicense. You may add Your own attribution notices within Derivative Works that\nYou distribute, alongside or as an addendum to the NOTICE text from the Work,\nprovided that such additional attribution notices cannot be construed as\nmodifying the License.\nYou may add Your own copyright statement to Your modifications and may provide\nadditional or different license terms and conditions for use, reproduction, or\ndistribution of Your modifications, or for any such Derivative Works as a whole,\nprovided Your use, reproduction, and distribution of the Work otherwise complies\nwith the conditions stated in this License.\n\n5. Submission of Contributions.\n\nUnless You explicitly state otherwise, any Contribution intentionally submitted\nfor inclusion in the Work by You to the Licensor shall be under the terms and\nconditions of this License, without any additional terms or conditions.\nNotwithstanding the above, nothing herein shall supersede or modify the terms of\nany separate license agreement you may have executed with Licensor regarding\nsuch Contributions.\n\n6. Trademarks.\n\nThis License does not grant permission to use the trade names, trademarks,\nservice marks, or product names of the Licensor, except as required for\nreasonable and customary use in describing the origin of the Work and\nreproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty.\n\nUnless required by applicable law or agreed to in writing, Licensor provides the\nWork (and each Contributor provides its Contributions) on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,\nincluding, without limitation, any warranties or conditions of TITLE,\nNON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are\nsolely responsible for determining the appropriateness of using or\nredistributing the Work and assume any risks associated with Your exercise of\npermissions under this License.\n\n8. Limitation of Liability.\n\nIn no event and under no legal theory, whether in tort (including negligence),\ncontract, or otherwise, unless required by applicable law (such as deliberate\nand grossly negligent acts) or agreed to in writing, shall any Contributor be\nliable to You for damages, including any direct, indirect, special, incidental,\nor consequential damages of any character arising as a result of this License or\nout of the use or inability to use the Work (including but not limited to\ndamages for loss of goodwill, work stoppage, computer failure or malfunction, or\nany and all other commercial damages or losses), even if such Contributor has\nbeen advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability.\n\nWhile redistributing the Work or Derivative Works thereof, You may choose to\noffer, and charge a fee for, acceptance of support, warranty, indemnity, or\nother liability obligations and/or rights consistent with this License. However,\nin accepting such obligations, You may act only on Your own behalf and on Your\nsole responsibility, not on behalf of any other Contributor, and only if You\nagree to indemnify, defend, and hold each Contributor harmless for any liability\nincurred by, or claims asserted against, such Contributor by reason of your\naccepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nAPPENDIX: How to apply the Apache License to your work\n\nTo apply the Apache License to your work, attach the following boilerplate\nnotice, with the fields enclosed by brackets \"[]\" replaced with your own\nidentifying information. (Don't include the brackets!) The text should be\nenclosed in the appropriate comment syntax for the file format. We also\nrecommend that a file or class name and description of purpose be included on\nthe same \"printed page\" as the copyright notice for easier identification within\nthird-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n", - "ecosystem": "npm" - }, - { - "name": "@img/sharp-linuxmusl-x64", - "version": "0.35.3", - "license": "Apache-2.0", - "repository": "https://github.com/lovell/sharp", - "license_text": "Apache License\nVersion 2.0, January 2004\nhttp://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n\"License\" shall mean the terms and conditions for use, reproduction, and\ndistribution as defined by Sections 1 through 9 of this document.\n\n\"Licensor\" shall mean the copyright owner or entity authorized by the copyright\nowner that is granting the License.\n\n\"Legal Entity\" shall mean the union of the acting entity and all other entities\nthat control, are controlled by, or are under common control with that entity.\nFor the purposes of this definition, \"control\" means (i) the power, direct or\nindirect, to cause the direction or management of such entity, whether by\ncontract or otherwise, or (ii) ownership of fifty percent (50%) or more of the\noutstanding shares, or (iii) beneficial ownership of such entity.\n\n\"You\" (or \"Your\") shall mean an individual or Legal Entity exercising\npermissions granted by this License.\n\n\"Source\" form shall mean the preferred form for making modifications, including\nbut not limited to software source code, documentation source, and configuration\nfiles.\n\n\"Object\" form shall mean any form resulting from mechanical transformation or\ntranslation of a Source form, including but not limited to compiled object code,\ngenerated documentation, and conversions to other media types.\n\n\"Work\" shall mean the work of authorship, whether in Source or Object form, made\navailable under the License, as indicated by a copyright notice that is included\nin or attached to the work (an example is provided in the Appendix below).\n\n\"Derivative Works\" shall mean any work, whether in Source or Object form, that\nis based on (or derived from) the Work and for which the editorial revisions,\nannotations, elaborations, or other modifications represent, as a whole, an\noriginal work of authorship. For the purposes of this License, Derivative Works\nshall not include works that remain separable from, or merely link (or bind by\nname) to the interfaces of, the Work and Derivative Works thereof.\n\n\"Contribution\" shall mean any work of authorship, including the original version\nof the Work and any modifications or additions to that Work or Derivative Works\nthereof, that is intentionally submitted to Licensor for inclusion in the Work\nby the copyright owner or by an individual or Legal Entity authorized to submit\non behalf of the copyright owner. For the purposes of this definition,\n\"submitted\" means any form of electronic, verbal, or written communication sent\nto the Licensor or its representatives, including but not limited to\ncommunication on electronic mailing lists, source code control systems, and\nissue tracking systems that are managed by, or on behalf of, the Licensor for\nthe purpose of discussing and improving the Work, but excluding communication\nthat is conspicuously marked or otherwise designated in writing by the copyright\nowner as \"Not a Contribution.\"\n\n\"Contributor\" shall mean Licensor and any individual or Legal Entity on behalf\nof whom a Contribution has been received by Licensor and subsequently\nincorporated within the Work.\n\n2. Grant of Copyright License.\n\nSubject to the terms and conditions of this License, each Contributor hereby\ngrants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,\nirrevocable copyright license to reproduce, prepare Derivative Works of,\npublicly display, publicly perform, sublicense, and distribute the Work and such\nDerivative Works in Source or Object form.\n\n3. Grant of Patent License.\n\nSubject to the terms and conditions of this License, each Contributor hereby\ngrants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,\nirrevocable (except as stated in this section) patent license to make, have\nmade, use, offer to sell, sell, import, and otherwise transfer the Work, where\nsuch license applies only to those patent claims licensable by such Contributor\nthat are necessarily infringed by their Contribution(s) alone or by combination\nof their Contribution(s) with the Work to which such Contribution(s) was\nsubmitted. If You institute patent litigation against any entity (including a\ncross-claim or counterclaim in a lawsuit) alleging that the Work or a\nContribution incorporated within the Work constitutes direct or contributory\npatent infringement, then any patent licenses granted to You under this License\nfor that Work shall terminate as of the date such litigation is filed.\n\n4. Redistribution.\n\nYou may reproduce and distribute copies of the Work or Derivative Works thereof\nin any medium, with or without modifications, and in Source or Object form,\nprovided that You meet the following conditions:\n\nYou must give any other recipients of the Work or Derivative Works a copy of\nthis License; and\nYou must cause any modified files to carry prominent notices stating that You\nchanged the files; and\nYou must retain, in the Source form of any Derivative Works that You distribute,\nall copyright, patent, trademark, and attribution notices from the Source form\nof the Work, excluding those notices that do not pertain to any part of the\nDerivative Works; and\nIf the Work includes a \"NOTICE\" text file as part of its distribution, then any\nDerivative Works that You distribute must include a readable copy of the\nattribution notices contained within such NOTICE file, excluding those notices\nthat do not pertain to any part of the Derivative Works, in at least one of the\nfollowing places: within a NOTICE text file distributed as part of the\nDerivative Works; within the Source form or documentation, if provided along\nwith the Derivative Works; or, within a display generated by the Derivative\nWorks, if and wherever such third-party notices normally appear. The contents of\nthe NOTICE file are for informational purposes only and do not modify the\nLicense. You may add Your own attribution notices within Derivative Works that\nYou distribute, alongside or as an addendum to the NOTICE text from the Work,\nprovided that such additional attribution notices cannot be construed as\nmodifying the License.\nYou may add Your own copyright statement to Your modifications and may provide\nadditional or different license terms and conditions for use, reproduction, or\ndistribution of Your modifications, or for any such Derivative Works as a whole,\nprovided Your use, reproduction, and distribution of the Work otherwise complies\nwith the conditions stated in this License.\n\n5. Submission of Contributions.\n\nUnless You explicitly state otherwise, any Contribution intentionally submitted\nfor inclusion in the Work by You to the Licensor shall be under the terms and\nconditions of this License, without any additional terms or conditions.\nNotwithstanding the above, nothing herein shall supersede or modify the terms of\nany separate license agreement you may have executed with Licensor regarding\nsuch Contributions.\n\n6. Trademarks.\n\nThis License does not grant permission to use the trade names, trademarks,\nservice marks, or product names of the Licensor, except as required for\nreasonable and customary use in describing the origin of the Work and\nreproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty.\n\nUnless required by applicable law or agreed to in writing, Licensor provides the\nWork (and each Contributor provides its Contributions) on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,\nincluding, without limitation, any warranties or conditions of TITLE,\nNON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are\nsolely responsible for determining the appropriateness of using or\nredistributing the Work and assume any risks associated with Your exercise of\npermissions under this License.\n\n8. Limitation of Liability.\n\nIn no event and under no legal theory, whether in tort (including negligence),\ncontract, or otherwise, unless required by applicable law (such as deliberate\nand grossly negligent acts) or agreed to in writing, shall any Contributor be\nliable to You for damages, including any direct, indirect, special, incidental,\nor consequential damages of any character arising as a result of this License or\nout of the use or inability to use the Work (including but not limited to\ndamages for loss of goodwill, work stoppage, computer failure or malfunction, or\nany and all other commercial damages or losses), even if such Contributor has\nbeen advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability.\n\nWhile redistributing the Work or Derivative Works thereof, You may choose to\noffer, and charge a fee for, acceptance of support, warranty, indemnity, or\nother liability obligations and/or rights consistent with this License. However,\nin accepting such obligations, You may act only on Your own behalf and on Your\nsole responsibility, not on behalf of any other Contributor, and only if You\nagree to indemnify, defend, and hold each Contributor harmless for any liability\nincurred by, or claims asserted against, such Contributor by reason of your\naccepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nAPPENDIX: How to apply the Apache License to your work\n\nTo apply the Apache License to your work, attach the following boilerplate\nnotice, with the fields enclosed by brackets \"[]\" replaced with your own\nidentifying information. (Don't include the brackets!) The text should be\nenclosed in the appropriate comment syntax for the file format. We also\nrecommend that a file or class name and description of purpose be included on\nthe same \"printed page\" as the copyright notice for easier identification within\nthird-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n", - "ecosystem": "npm" - }, { "name": "@inquirer/ansi", "version": "1.0.2", @@ -1747,86 +1685,6 @@ "license_text": "MIT License\n\nCopyright (c) 2026 The Pierre Computer Company\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n", "ecosystem": "npm" }, - { - "name": "@protobufjs/aspromise", - "version": "1.1.2", - "license": "BSD-3-Clause", - "repository": "https://github.com/dcodeIO/protobuf.js", - "license_text": "Copyright (c) 2016, Daniel Wirtz All rights reserved.\r\n\r\nRedistribution and use in source and binary forms, with or without\r\nmodification, are permitted provided that the following conditions are\r\nmet:\r\n\r\n* Redistributions of source code must retain the above copyright\r\n notice, this list of conditions and the following disclaimer.\r\n* Redistributions in binary form must reproduce the above copyright\r\n notice, this list of conditions and the following disclaimer in the\r\n documentation and/or other materials provided with the distribution.\r\n* Neither the name of its author, nor the names of its contributors\r\n may be used to endorse or promote products derived from this software\r\n without specific prior written permission.\r\n\r\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\r\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\r\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\r\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\r\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\r\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\r\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\r\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\r\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\r\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\r\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\r\n", - "ecosystem": "npm" - }, - { - "name": "@protobufjs/base64", - "version": "1.1.2", - "license": "BSD-3-Clause", - "repository": "https://github.com/dcodeIO/protobuf.js", - "license_text": "Copyright (c) 2016, Daniel Wirtz All rights reserved.\r\n\r\nRedistribution and use in source and binary forms, with or without\r\nmodification, are permitted provided that the following conditions are\r\nmet:\r\n\r\n* Redistributions of source code must retain the above copyright\r\n notice, this list of conditions and the following disclaimer.\r\n* Redistributions in binary form must reproduce the above copyright\r\n notice, this list of conditions and the following disclaimer in the\r\n documentation and/or other materials provided with the distribution.\r\n* Neither the name of its author, nor the names of its contributors\r\n may be used to endorse or promote products derived from this software\r\n without specific prior written permission.\r\n\r\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\r\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\r\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\r\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\r\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\r\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\r\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\r\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\r\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\r\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\r\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\r\n", - "ecosystem": "npm" - }, - { - "name": "@protobufjs/codegen", - "version": "2.0.5", - "license": "BSD-3-Clause", - "repository": "https://github.com/dcodeIO/protobuf.js", - "license_text": "Copyright (c) 2016, Daniel Wirtz All rights reserved.\r\n\r\nRedistribution and use in source and binary forms, with or without\r\nmodification, are permitted provided that the following conditions are\r\nmet:\r\n\r\n* Redistributions of source code must retain the above copyright\r\n notice, this list of conditions and the following disclaimer.\r\n* Redistributions in binary form must reproduce the above copyright\r\n notice, this list of conditions and the following disclaimer in the\r\n documentation and/or other materials provided with the distribution.\r\n* Neither the name of its author, nor the names of its contributors\r\n may be used to endorse or promote products derived from this software\r\n without specific prior written permission.\r\n\r\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\r\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\r\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\r\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\r\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\r\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\r\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\r\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\r\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\r\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\r\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\r\n", - "ecosystem": "npm" - }, - { - "name": "@protobufjs/eventemitter", - "version": "1.1.1", - "license": "BSD-3-Clause", - "repository": "https://github.com/dcodeIO/protobuf.js", - "license_text": "Copyright (c) 2016, Daniel Wirtz All rights reserved.\r\n\r\nRedistribution and use in source and binary forms, with or without\r\nmodification, are permitted provided that the following conditions are\r\nmet:\r\n\r\n* Redistributions of source code must retain the above copyright\r\n notice, this list of conditions and the following disclaimer.\r\n* Redistributions in binary form must reproduce the above copyright\r\n notice, this list of conditions and the following disclaimer in the\r\n documentation and/or other materials provided with the distribution.\r\n* Neither the name of its author, nor the names of its contributors\r\n may be used to endorse or promote products derived from this software\r\n without specific prior written permission.\r\n\r\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\r\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\r\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\r\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\r\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\r\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\r\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\r\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\r\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\r\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\r\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\r\n", - "ecosystem": "npm" - }, - { - "name": "@protobufjs/fetch", - "version": "1.1.1", - "license": "BSD-3-Clause", - "repository": "https://github.com/dcodeIO/protobuf.js", - "license_text": "Copyright (c) 2016, Daniel Wirtz All rights reserved.\r\n\r\nRedistribution and use in source and binary forms, with or without\r\nmodification, are permitted provided that the following conditions are\r\nmet:\r\n\r\n* Redistributions of source code must retain the above copyright\r\n notice, this list of conditions and the following disclaimer.\r\n* Redistributions in binary form must reproduce the above copyright\r\n notice, this list of conditions and the following disclaimer in the\r\n documentation and/or other materials provided with the distribution.\r\n* Neither the name of its author, nor the names of its contributors\r\n may be used to endorse or promote products derived from this software\r\n without specific prior written permission.\r\n\r\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\r\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\r\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\r\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\r\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\r\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\r\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\r\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\r\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\r\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\r\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\r\n", - "ecosystem": "npm" - }, - { - "name": "@protobufjs/float", - "version": "1.0.2", - "license": "BSD-3-Clause", - "repository": "https://github.com/dcodeIO/protobuf.js", - "license_text": "Copyright (c) 2016, Daniel Wirtz All rights reserved.\r\n\r\nRedistribution and use in source and binary forms, with or without\r\nmodification, are permitted provided that the following conditions are\r\nmet:\r\n\r\n* Redistributions of source code must retain the above copyright\r\n notice, this list of conditions and the following disclaimer.\r\n* Redistributions in binary form must reproduce the above copyright\r\n notice, this list of conditions and the following disclaimer in the\r\n documentation and/or other materials provided with the distribution.\r\n* Neither the name of its author, nor the names of its contributors\r\n may be used to endorse or promote products derived from this software\r\n without specific prior written permission.\r\n\r\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\r\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\r\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\r\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\r\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\r\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\r\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\r\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\r\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\r\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\r\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\r\n", - "ecosystem": "npm" - }, - { - "name": "@protobufjs/inquire", - "version": "1.1.2", - "license": "BSD-3-Clause", - "repository": "https://github.com/dcodeIO/protobuf.js", - "license_text": "Copyright (c) 2016, Daniel Wirtz All rights reserved.\r\n\r\nRedistribution and use in source and binary forms, with or without\r\nmodification, are permitted provided that the following conditions are\r\nmet:\r\n\r\n* Redistributions of source code must retain the above copyright\r\n notice, this list of conditions and the following disclaimer.\r\n* Redistributions in binary form must reproduce the above copyright\r\n notice, this list of conditions and the following disclaimer in the\r\n documentation and/or other materials provided with the distribution.\r\n* Neither the name of its author, nor the names of its contributors\r\n may be used to endorse or promote products derived from this software\r\n without specific prior written permission.\r\n\r\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\r\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\r\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\r\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\r\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\r\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\r\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\r\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\r\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\r\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\r\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\r\n", - "ecosystem": "npm" - }, - { - "name": "@protobufjs/path", - "version": "1.1.2", - "license": "BSD-3-Clause", - "repository": "https://github.com/dcodeIO/protobuf.js", - "license_text": "Copyright (c) 2016, Daniel Wirtz All rights reserved.\r\n\r\nRedistribution and use in source and binary forms, with or without\r\nmodification, are permitted provided that the following conditions are\r\nmet:\r\n\r\n* Redistributions of source code must retain the above copyright\r\n notice, this list of conditions and the following disclaimer.\r\n* Redistributions in binary form must reproduce the above copyright\r\n notice, this list of conditions and the following disclaimer in the\r\n documentation and/or other materials provided with the distribution.\r\n* Neither the name of its author, nor the names of its contributors\r\n may be used to endorse or promote products derived from this software\r\n without specific prior written permission.\r\n\r\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\r\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\r\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\r\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\r\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\r\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\r\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\r\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\r\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\r\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\r\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\r\n", - "ecosystem": "npm" - }, - { - "name": "@protobufjs/pool", - "version": "1.1.0", - "license": "BSD-3-Clause", - "repository": "https://github.com/dcodeIO/protobuf.js", - "license_text": "Copyright (c) 2016, Daniel Wirtz All rights reserved.\r\n\r\nRedistribution and use in source and binary forms, with or without\r\nmodification, are permitted provided that the following conditions are\r\nmet:\r\n\r\n* Redistributions of source code must retain the above copyright\r\n notice, this list of conditions and the following disclaimer.\r\n* Redistributions in binary form must reproduce the above copyright\r\n notice, this list of conditions and the following disclaimer in the\r\n documentation and/or other materials provided with the distribution.\r\n* Neither the name of its author, nor the names of its contributors\r\n may be used to endorse or promote products derived from this software\r\n without specific prior written permission.\r\n\r\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\r\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\r\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\r\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\r\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\r\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\r\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\r\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\r\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\r\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\r\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\r\n", - "ecosystem": "npm" - }, - { - "name": "@protobufjs/utf8", - "version": "1.1.1", - "license": "BSD-3-Clause", - "repository": "https://github.com/dcodeIO/protobuf.js", - "license_text": "Copyright (c) 2016, Daniel Wirtz All rights reserved.\r\n\r\nRedistribution and use in source and binary forms, with or without\r\nmodification, are permitted provided that the following conditions are\r\nmet:\r\n\r\n* Redistributions of source code must retain the above copyright\r\n notice, this list of conditions and the following disclaimer.\r\n* Redistributions in binary form must reproduce the above copyright\r\n notice, this list of conditions and the following disclaimer in the\r\n documentation and/or other materials provided with the distribution.\r\n* Neither the name of its author, nor the names of its contributors\r\n may be used to endorse or promote products derived from this software\r\n without specific prior written permission.\r\n\r\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\r\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\r\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\r\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\r\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\r\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\r\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\r\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\r\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\r\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\r\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\r\n", - "ecosystem": "npm" - }, { "name": "@radix-ui/number", "version": "1.1.1", @@ -3362,14 +3220,6 @@ "license_text": "MIT License\n\nCopyright (C) 2012-2022 by various contributors (see AUTHORS)\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n", "ecosystem": "npm" }, - { - "name": "adm-zip", - "version": "0.6.0", - "license": "MIT", - "repository": "https://github.com/cthackers/adm-zip", - "license_text": "MIT License\n\nCopyright (c) 2012 Another-D-Mention Software and other contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n", - "ecosystem": "npm" - }, { "name": "agent-base", "version": "7.1.4", @@ -3482,14 +3332,6 @@ "license_text": "(The MIT License)\n\nCopyright (c) 2014 Jonathan Ong \nCopyright (c) 2014-2015 Douglas Christopher Wilson \n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n'Software'), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.\nIN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY\nCLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,\nTORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE\nSOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n", "ecosystem": "npm" }, - { - "name": "boolean", - "version": "3.2.0", - "license": "MIT", - "repository": "git://github.com/thenativeweb/boolean", - "license_text": "The MIT License (MIT)\nCopyright (c) 2014-2022 the native web.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n", - "ecosystem": "npm" - }, { "name": "brace-expansion", "version": "5.0.8", @@ -4269,14 +4111,6 @@ "license_text": "MIT License\n\nCopyright (c) Sindre Sorhus (https://sindresorhus.com)\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n", "ecosystem": "npm" }, - { - "name": "define-data-property", - "version": "1.1.4", - "license": "MIT", - "repository": "https://github.com/ljharb/define-data-property", - "license_text": "MIT License\n\nCopyright (c) 2023 Jordan Harband\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n", - "ecosystem": "npm" - }, { "name": "define-lazy-prop", "version": "3.0.0", @@ -4285,14 +4119,6 @@ "license_text": "MIT License\n\nCopyright (c) Sindre Sorhus (https://sindresorhus.com)\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n", "ecosystem": "npm" }, - { - "name": "define-properties", - "version": "1.2.1", - "license": "MIT", - "repository": "git://github.com/ljharb/define-properties", - "license_text": "The MIT License (MIT)\n\nCopyright (C) 2015 Jordan Harband\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.", - "ecosystem": "npm" - }, { "name": "delaunator", "version": "5.0.1", @@ -4325,14 +4151,6 @@ "license_text": " Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"{}\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright {yyyy} {name of copyright owner}\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n", "ecosystem": "npm" }, - { - "name": "detect-node", - "version": "2.1.0", - "license": "MIT", - "repository": "https://github.com/iliakan/detect-node", - "license_text": "MIT License\n\nCopyright (c) 2017 Ilya Kantor\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n", - "ecosystem": "npm" - }, { "name": "detect-node-es", "version": "1.1.0", @@ -4541,14 +4359,6 @@ "license_text": "MIT License\n\nCopyright (c) 2024 Jordan Harband\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n", "ecosystem": "npm" }, - { - "name": "es6-error", - "version": "4.1.1", - "license": "MIT", - "repository": "https://github.com/bjyoungblood/es6-error", - "license_text": "The MIT License (MIT)\n\nCopyright (c) 2015 Ben Youngblood\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n", - "ecosystem": "npm" - }, { "name": "escalade", "version": "3.2.0", @@ -4748,14 +4558,6 @@ "license_text": "(The MIT License)\n\nCopyright (c) 2014-2022 Douglas Christopher Wilson \n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n'Software'), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.\nIN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY\nCLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,\nTORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE\nSOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n", "ecosystem": "npm" }, - { - "name": "flatbuffers", - "version": "25.9.23", - "license": "Apache-2.0", - "repository": "https://github.com/google/flatbuffers", - "license_text": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n", - "ecosystem": "npm" - }, { "name": "formdata-polyfill", "version": "4.0.10", @@ -4900,22 +4702,6 @@ "license_text": "The ISC License\n\nCopyright (c) 2015, 2019 Elan Shanker\n\nPermission to use, copy, modify, and/or distribute this software for any\npurpose with or without fee is hereby granted, provided that the above\ncopyright notice and this permission notice appear in all copies.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES\nWITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\nMERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR\nANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\nWHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\nACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR\nIN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n", "ecosystem": "npm" }, - { - "name": "global-agent", - "version": "3.0.0", - "license": "BSD-3-Clause", - "repository": "https://github.com/gajus/global-agent", - "license_text": "Copyright (c) 2019, Gajus Kuizinas (http://gajus.com/)\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n * Redistributions of source code must retain the above copyright\n notice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above copyright\n notice, this list of conditions and the following disclaimer in the\n documentation and/or other materials provided with the distribution.\n * Neither the name of the Gajus Kuizinas (http://gajus.com/) nor the\n names of its contributors may be used to endorse or promote products\n derived from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL ANUARY BE LIABLE FOR ANY\nDIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND\nON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n", - "ecosystem": "npm" - }, - { - "name": "globalthis", - "version": "1.0.4", - "license": "MIT", - "repository": "git://github.com/ljharb/System.global", - "license_text": "The MIT License (MIT)\n\nCopyright (c) 2016 Jordan Harband\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n", - "ecosystem": "npm" - }, { "name": "gopd", "version": "1.2.0", @@ -4940,13 +4726,6 @@ "license_text": "MIT License\n\nCopyright (c) GraphQL Contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n", "ecosystem": "npm" }, - { - "name": "guid-typescript", - "version": "1.0.9", - "license": "ISC", - "repository": "https://github.com/NicolasDeveloper/guid-typescript", - "ecosystem": "npm" - }, { "name": "hachure-fill", "version": "0.5.2", @@ -4955,14 +4734,6 @@ "license_text": "MIT License\n\nCopyright (c) 2023 Preet Shihn\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n", "ecosystem": "npm" }, - { - "name": "has-property-descriptors", - "version": "1.0.2", - "license": "MIT", - "repository": "https://github.com/inspect-js/has-property-descriptors", - "license_text": "MIT License\n\nCopyright (c) 2022 Inspect JS\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n", - "ecosystem": "npm" - }, { "name": "has-symbols", "version": "1.1.0", @@ -5480,14 +5251,6 @@ "license_text": "BSD 2-Clause License\r\n\r\nOriginal source code is copyright (c) 2019-2025 Remy Rylan\r\n\r\n\r\nAll JSON Schema documentation and descriptions are copyright (c):\r\n\r\n2009 [draft-0] IETF Trust , Kris Zyp ,\r\nand SitePen (USA) .\r\n\r\n2009 [draft-1] IETF Trust , Kris Zyp ,\r\nand SitePen (USA) .\r\n\r\n2010 [draft-2] IETF Trust , Kris Zyp ,\r\nand SitePen (USA) .\r\n\r\n2010 [draft-3] IETF Trust , Kris Zyp ,\r\nGary Court , and SitePen (USA) .\r\n\r\n2013 [draft-4] IETF Trust ), Francis Galiegue\r\n, Kris Zyp , Gary Court\r\n, and SitePen (USA) .\r\n\r\n2018 [draft-7] IETF Trust , Austin Wright ,\r\nHenry Andrews , Geraint Luff , and\r\nCloudflare, Inc. .\r\n\r\n2019 [draft-2019-09] IETF Trust , Austin Wright\r\n, Henry Andrews , Ben Hutton\r\n, and Greg Dennis .\r\n\r\n2020 [draft-2020-12] IETF Trust , Austin Wright\r\n, Henry Andrews , Ben Hutton\r\n, and Greg Dennis .\r\n\r\nAll rights reserved.\r\n\r\nRedistribution and use in source and binary forms, with or without modification,\r\nare permitted provided that the following conditions are met:\r\n\r\n1. Redistributions of source code must retain the above copyright notice, this\r\n list of conditions and the following disclaimer.\r\n\r\n2. Redistributions in binary form must reproduce the above copyright notice,\r\n this list of conditions and the following disclaimer in the documentation\r\n and/or other materials provided with the distribution.\r\n\r\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\r\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\r\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\r\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR\r\nANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\r\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\r\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\r\nANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\r\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\r\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\r\n", "ecosystem": "npm" }, - { - "name": "json-stringify-safe", - "version": "5.0.1", - "license": "ISC", - "repository": "git://github.com/isaacs/json-stringify-safe", - "license_text": "The ISC License\n\nCopyright (c) Isaac Z. Schlueter and Contributors\n\nPermission to use, copy, modify, and/or distribute this software for any\npurpose with or without fee is hereby granted, provided that the above\ncopyright notice and this permission notice appear in all copies.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES\nWITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\nMERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR\nANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\nWHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\nACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR\nIN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n", - "ecosystem": "npm" - }, { "name": "json5", "version": "2.2.3", @@ -5616,14 +5379,6 @@ "license_text": "MIT License\n\nCopyright (c) Sindre Sorhus (https://sindresorhus.com)\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n", "ecosystem": "npm" }, - { - "name": "long", - "version": "5.3.2", - "license": "Apache-2.0", - "repository": "https://github.com/dcodeIO/long.js", - "license_text": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n", - "ecosystem": "npm" - }, { "name": "longest-streak", "version": "3.1.0", @@ -5703,14 +5458,6 @@ "license_text": "# License information\n\n## Contribution License Agreement\n\nIf you contribute code to this project, you are implicitly allowing your code\nto be distributed under the MIT license. You are also implicitly verifying that\nall code is your original work. ``\n\n## Marked\n\nCopyright (c) 2018+, MarkedJS (https://github.com/markedjs/)\nCopyright (c) 2011-2018, Christopher Jeffrey (https://github.com/chjj/)\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n\n## Markdown\n\nCopyright © 2004, John Gruber\nhttp://daringfireball.net/\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.\n* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.\n* Neither the name “Markdown” nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.\n\nThis software is provided by the copyright holders and contributors “as is” and any express or implied warranties, including, but not limited to, the implied warranties of merchantability and fitness for a particular purpose are disclaimed. In no event shall the copyright owner or contributors be liable for any direct, indirect, incidental, special, exemplary, or consequential damages (including, but not limited to, procurement of substitute goods or services; loss of use, data, or profits; or business interruption) however caused and on any theory of liability, whether in contract, strict liability, or tort (including negligence or otherwise) arising in any way out of the use of this software, even if advised of the possibility of such damage.\n", "ecosystem": "npm" }, - { - "name": "matcher", - "version": "3.0.0", - "license": "MIT", - "repository": "sindresorhus/matcher", - "license_text": "MIT License\n\nCopyright (c) Sindre Sorhus (https://sindresorhus.com)\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n", - "ecosystem": "npm" - }, { "name": "math-intrinsics", "version": "1.1.0", @@ -6287,14 +6034,6 @@ "license_text": "MIT License\n\nCopyright (c) 2013 James Halliday\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n", "ecosystem": "npm" }, - { - "name": "object-keys", - "version": "1.1.1", - "license": "MIT", - "repository": "git://github.com/ljharb/object-keys", - "license_text": "The MIT License (MIT)\n\nCopyright (C) 2013 Jordan Harband\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.", - "ecosystem": "npm" - }, { "name": "object-treeify", "version": "1.1.33", @@ -6351,34 +6090,6 @@ "license_text": "MIT License\n\nCopyright (c) 2025 Steven Levithan\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n", "ecosystem": "npm" }, - { - "name": "onnxruntime-common", - "version": "1.24.0-dev.20251116-b39e144322", - "license": "MIT", - "repository": "https://github.com/Microsoft/onnxruntime", - "ecosystem": "npm" - }, - { - "name": "onnxruntime-common", - "version": "1.24.3", - "license": "MIT", - "repository": "https://github.com/Microsoft/onnxruntime", - "ecosystem": "npm" - }, - { - "name": "onnxruntime-node", - "version": "1.24.3", - "license": "MIT", - "repository": "https://github.com/Microsoft/onnxruntime", - "ecosystem": "npm" - }, - { - "name": "onnxruntime-web", - "version": "1.26.0-dev.20260416-b7804b056c", - "license": "MIT", - "repository": "https://github.com/Microsoft/onnxruntime", - "ecosystem": "npm" - }, { "name": "open", "version": "11.0.0", @@ -6563,14 +6274,6 @@ "license_text": "MIT License\n\nCopyright (c) Pooya Parsa - Daniel Roe \n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n--------------------------------------------------------------------------------\n\nCopyright Joyent, Inc. and other Node contributors.\n\nPermission is hereby granted, free of charge, to any person obtaining a\ncopy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to permit\npersons to whom the Software is furnished to do so, subject to the\nfollowing conditions:\n\nThe above copyright notice and this permission notice shall be included\nin all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS\nOR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN\nNO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,\nDAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR\nOTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE\nUSE OR OTHER DEALINGS IN THE SOFTWARE.\n", "ecosystem": "npm" }, - { - "name": "platform", - "version": "1.3.6", - "license": "MIT", - "repository": "bestiejs/platform.js", - "license_text": "Copyright 2014-2020 Benjamin Tan\nCopyright 2011-2013 John-David Dalton\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n", - "ecosystem": "npm" - }, { "name": "points-on-curve", "version": "0.2.0", @@ -6787,14 +6490,6 @@ "license_text": "Copyright (C) 2015-2017 by Marijn Haverbeke and others\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n", "ecosystem": "npm" }, - { - "name": "protobufjs", - "version": "7.6.1", - "license": "BSD-3-Clause", - "repository": "protobufjs/protobuf.js", - "license_text": "This license applies to all parts of protobuf.js except those files\neither explicitly including or referencing a different license or\nlocated in a directory containing a different LICENSE file.\n\n---\n\nCopyright (c) 2016, Daniel Wirtz All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n* Redistributions of source code must retain the above copyright\n notice, this list of conditions and the following disclaimer.\n* Redistributions in binary form must reproduce the above copyright\n notice, this list of conditions and the following disclaimer in the\n documentation and/or other materials provided with the distribution.\n* Neither the name of its author, nor the names of its contributors\n may be used to endorse or promote products derived from this software\n without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n---\n\nCode generated by the command line utilities is owned by the owner\nof the input file used when generating it. This code is not\nstandalone and requires a support library to be linked with it. This\nsupport library is itself covered by the above license.\n", - "ecosystem": "npm" - }, { "name": "proxy-addr", "version": "2.0.7", @@ -7112,14 +6807,6 @@ "license_text": "The MIT License (MIT)\n\nCopyright (c) 2015-2024 Matteo Collina\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n", "ecosystem": "npm" }, - { - "name": "roarr", - "version": "2.15.4", - "license": "BSD-3-Clause", - "repository": "git@github.com:gajus/roarr", - "license_text": "Copyright (c) 2019, Gajus Kuizinas (http://gajus.com/)\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n * Redistributions of source code must retain the above copyright\n notice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above copyright\n notice, this list of conditions and the following disclaimer in the\n documentation and/or other materials provided with the distribution.\n * Neither the name of the Gajus Kuizinas (http://gajus.com/) nor the\n names of its contributors may be used to endorse or promote products\n derived from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL ANUARY BE LIABLE FOR ANY\nDIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND\nON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n", - "ecosystem": "npm" - }, { "name": "robust-predicates", "version": "3.0.2", @@ -7216,14 +6903,6 @@ "license_text": "The ISC License\n\nCopyright (c) Isaac Z. Schlueter and Contributors\n\nPermission to use, copy, modify, and/or distribute this software for any\npurpose with or without fee is hereby granted, provided that the above\ncopyright notice and this permission notice appear in all copies.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES\nWITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\nMERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR\nANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\nWHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\nACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR\nIN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n", "ecosystem": "npm" }, - { - "name": "semver-compare", - "version": "1.0.0", - "license": "MIT", - "repository": "git://github.com/substack/semver-compare", - "license_text": "This software is released under the MIT license:\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n", - "ecosystem": "npm" - }, { "name": "send", "version": "1.2.1", @@ -7232,14 +6911,6 @@ "license_text": "(The MIT License)\n\nCopyright (c) 2012 TJ Holowaychuk\nCopyright (c) 2014-2022 Douglas Christopher Wilson\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n'Software'), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.\nIN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY\nCLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,\nTORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE\nSOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n", "ecosystem": "npm" }, - { - "name": "serialize-error", - "version": "7.0.1", - "license": "MIT", - "repository": "sindresorhus/serialize-error", - "license_text": "MIT License\n\nCopyright (c) Sindre Sorhus (https://sindresorhus.com)\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n", - "ecosystem": "npm" - }, { "name": "serve-static", "version": "2.2.1", @@ -7264,14 +6935,6 @@ "license_text": "MIT License\n\nCopyright (c) 2023 shadcn\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n", "ecosystem": "npm" }, - { - "name": "sharp", - "version": "0.35.3", - "license": "Apache-2.0", - "repository": "https://github.com/lovell/sharp", - "license_text": "Apache License\nVersion 2.0, January 2004\nhttp://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n\"License\" shall mean the terms and conditions for use, reproduction, and\ndistribution as defined by Sections 1 through 9 of this document.\n\n\"Licensor\" shall mean the copyright owner or entity authorized by the copyright\nowner that is granting the License.\n\n\"Legal Entity\" shall mean the union of the acting entity and all other entities\nthat control, are controlled by, or are under common control with that entity.\nFor the purposes of this definition, \"control\" means (i) the power, direct or\nindirect, to cause the direction or management of such entity, whether by\ncontract or otherwise, or (ii) ownership of fifty percent (50%) or more of the\noutstanding shares, or (iii) beneficial ownership of such entity.\n\n\"You\" (or \"Your\") shall mean an individual or Legal Entity exercising\npermissions granted by this License.\n\n\"Source\" form shall mean the preferred form for making modifications, including\nbut not limited to software source code, documentation source, and configuration\nfiles.\n\n\"Object\" form shall mean any form resulting from mechanical transformation or\ntranslation of a Source form, including but not limited to compiled object code,\ngenerated documentation, and conversions to other media types.\n\n\"Work\" shall mean the work of authorship, whether in Source or Object form, made\navailable under the License, as indicated by a copyright notice that is included\nin or attached to the work (an example is provided in the Appendix below).\n\n\"Derivative Works\" shall mean any work, whether in Source or Object form, that\nis based on (or derived from) the Work and for which the editorial revisions,\nannotations, elaborations, or other modifications represent, as a whole, an\noriginal work of authorship. For the purposes of this License, Derivative Works\nshall not include works that remain separable from, or merely link (or bind by\nname) to the interfaces of, the Work and Derivative Works thereof.\n\n\"Contribution\" shall mean any work of authorship, including the original version\nof the Work and any modifications or additions to that Work or Derivative Works\nthereof, that is intentionally submitted to Licensor for inclusion in the Work\nby the copyright owner or by an individual or Legal Entity authorized to submit\non behalf of the copyright owner. For the purposes of this definition,\n\"submitted\" means any form of electronic, verbal, or written communication sent\nto the Licensor or its representatives, including but not limited to\ncommunication on electronic mailing lists, source code control systems, and\nissue tracking systems that are managed by, or on behalf of, the Licensor for\nthe purpose of discussing and improving the Work, but excluding communication\nthat is conspicuously marked or otherwise designated in writing by the copyright\nowner as \"Not a Contribution.\"\n\n\"Contributor\" shall mean Licensor and any individual or Legal Entity on behalf\nof whom a Contribution has been received by Licensor and subsequently\nincorporated within the Work.\n\n2. Grant of Copyright License.\n\nSubject to the terms and conditions of this License, each Contributor hereby\ngrants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,\nirrevocable copyright license to reproduce, prepare Derivative Works of,\npublicly display, publicly perform, sublicense, and distribute the Work and such\nDerivative Works in Source or Object form.\n\n3. Grant of Patent License.\n\nSubject to the terms and conditions of this License, each Contributor hereby\ngrants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,\nirrevocable (except as stated in this section) patent license to make, have\nmade, use, offer to sell, sell, import, and otherwise transfer the Work, where\nsuch license applies only to those patent claims licensable by such Contributor\nthat are necessarily infringed by their Contribution(s) alone or by combination\nof their Contribution(s) with the Work to which such Contribution(s) was\nsubmitted. If You institute patent litigation against any entity (including a\ncross-claim or counterclaim in a lawsuit) alleging that the Work or a\nContribution incorporated within the Work constitutes direct or contributory\npatent infringement, then any patent licenses granted to You under this License\nfor that Work shall terminate as of the date such litigation is filed.\n\n4. Redistribution.\n\nYou may reproduce and distribute copies of the Work or Derivative Works thereof\nin any medium, with or without modifications, and in Source or Object form,\nprovided that You meet the following conditions:\n\nYou must give any other recipients of the Work or Derivative Works a copy of\nthis License; and\nYou must cause any modified files to carry prominent notices stating that You\nchanged the files; and\nYou must retain, in the Source form of any Derivative Works that You distribute,\nall copyright, patent, trademark, and attribution notices from the Source form\nof the Work, excluding those notices that do not pertain to any part of the\nDerivative Works; and\nIf the Work includes a \"NOTICE\" text file as part of its distribution, then any\nDerivative Works that You distribute must include a readable copy of the\nattribution notices contained within such NOTICE file, excluding those notices\nthat do not pertain to any part of the Derivative Works, in at least one of the\nfollowing places: within a NOTICE text file distributed as part of the\nDerivative Works; within the Source form or documentation, if provided along\nwith the Derivative Works; or, within a display generated by the Derivative\nWorks, if and wherever such third-party notices normally appear. The contents of\nthe NOTICE file are for informational purposes only and do not modify the\nLicense. You may add Your own attribution notices within Derivative Works that\nYou distribute, alongside or as an addendum to the NOTICE text from the Work,\nprovided that such additional attribution notices cannot be construed as\nmodifying the License.\nYou may add Your own copyright statement to Your modifications and may provide\nadditional or different license terms and conditions for use, reproduction, or\ndistribution of Your modifications, or for any such Derivative Works as a whole,\nprovided Your use, reproduction, and distribution of the Work otherwise complies\nwith the conditions stated in this License.\n\n5. Submission of Contributions.\n\nUnless You explicitly state otherwise, any Contribution intentionally submitted\nfor inclusion in the Work by You to the Licensor shall be under the terms and\nconditions of this License, without any additional terms or conditions.\nNotwithstanding the above, nothing herein shall supersede or modify the terms of\nany separate license agreement you may have executed with Licensor regarding\nsuch Contributions.\n\n6. Trademarks.\n\nThis License does not grant permission to use the trade names, trademarks,\nservice marks, or product names of the Licensor, except as required for\nreasonable and customary use in describing the origin of the Work and\nreproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty.\n\nUnless required by applicable law or agreed to in writing, Licensor provides the\nWork (and each Contributor provides its Contributions) on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,\nincluding, without limitation, any warranties or conditions of TITLE,\nNON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are\nsolely responsible for determining the appropriateness of using or\nredistributing the Work and assume any risks associated with Your exercise of\npermissions under this License.\n\n8. Limitation of Liability.\n\nIn no event and under no legal theory, whether in tort (including negligence),\ncontract, or otherwise, unless required by applicable law (such as deliberate\nand grossly negligent acts) or agreed to in writing, shall any Contributor be\nliable to You for damages, including any direct, indirect, special, incidental,\nor consequential damages of any character arising as a result of this License or\nout of the use or inability to use the Work (including but not limited to\ndamages for loss of goodwill, work stoppage, computer failure or malfunction, or\nany and all other commercial damages or losses), even if such Contributor has\nbeen advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability.\n\nWhile redistributing the Work or Derivative Works thereof, You may choose to\noffer, and charge a fee for, acceptance of support, warranty, indemnity, or\nother liability obligations and/or rights consistent with this License. However,\nin accepting such obligations, You may act only on Your own behalf and on Your\nsole responsibility, not on behalf of any other Contributor, and only if You\nagree to indemnify, defend, and hold each Contributor harmless for any liability\nincurred by, or claims asserted against, such Contributor by reason of your\naccepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nAPPENDIX: How to apply the Apache License to your work\n\nTo apply the Apache License to your work, attach the following boilerplate\nnotice, with the fields enclosed by brackets \"[]\" replaced with your own\nidentifying information. (Don't include the brackets!) The text should be\nenclosed in the appropriate comment syntax for the file format. We also\nrecommend that a file or class name and description of purpose be included on\nthe same \"printed page\" as the copyright notice for easier identification within\nthird-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n", - "ecosystem": "npm" - }, { "name": "shebang-command", "version": "2.0.0", @@ -7384,14 +7047,6 @@ "license_text": "(The MIT License)\n\nCopyright (c) 2016 Titus Wormer \n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n'Software'), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.\nIN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY\nCLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,\nTORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE\nSOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n", "ecosystem": "npm" }, - { - "name": "sprintf-js", - "version": "1.1.3", - "license": "BSD-3-Clause", - "repository": "https://github.com/alexei/sprintf.js", - "license_text": "Copyright (c) 2007-present, Alexandru Mărășteanu \nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n* Redistributions of source code must retain the above copyright\n notice, this list of conditions and the following disclaimer.\n* Redistributions in binary form must reproduce the above copyright\n notice, this list of conditions and the following disclaimer in the\n documentation and/or other materials provided with the distribution.\n* Neither the name of this software nor the names of its contributors may be\n used to endorse or promote products derived from this software without\n specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR\nANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND\nON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n", - "ecosystem": "npm" - }, { "name": "state-local", "version": "1.0.7", @@ -7679,21 +7334,6 @@ "license_text": "MIT License\n\nCopyright (c) 2025 Wombosvideo\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n", "ecosystem": "npm" }, - { - "name": "type-fest", - "version": "0.13.1", - "license": "(MIT OR CC0-1.0)", - "repository": "sindresorhus/type-fest", - "license_text": "MIT License\n\nCopyright (c) Sindre Sorhus (https:/sindresorhus.com)\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n", - "ecosystem": "npm" - }, - { - "name": "type-fest", - "version": "5.4.0", - "license": "(MIT OR CC0-1.0)", - "repository": "sindresorhus/type-fest", - "ecosystem": "npm" - }, { "name": "type-is", "version": "2.0.1", diff --git a/apps/web/hooks/use-ensure-user-settings.test.ts b/apps/web/hooks/use-ensure-user-settings.test.ts index 2185d0488d..d5ec8cc9ea 100644 --- a/apps/web/hooks/use-ensure-user-settings.test.ts +++ b/apps/web/hooks/use-ensure-user-settings.test.ts @@ -101,14 +101,6 @@ function makeUnloadedSettings(): UserSettingsState { systemMetricsDisplay: { showInTopbar: false, simplified: false }, appStatusBarEnabled: false, appStatusBarOrder: { leftItemIds: [], rightItemIds: [] }, - voiceMode: { - enabled: true, - engine: "auto", - language: "auto", - mode: "toggle", - autoSend: false, - whisperWebModel: "base", - }, lspAutoStartLanguages: [], lspAutoInstallLanguages: [], lspServerConfigs: {}, diff --git a/apps/web/hooks/use-voice-input.test.ts b/apps/web/hooks/use-voice-input.test.ts deleted file mode 100644 index 3137cceba9..0000000000 --- a/apps/web/hooks/use-voice-input.test.ts +++ /dev/null @@ -1,199 +0,0 @@ -import { act, renderHook, waitFor } from "@testing-library/react"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; - -// ── Hoisted mocks (defined before the modules they replace are evaluated) ── - -const voicePrefs = vi.hoisted(() => ({ - value: { - engine: "auto" as "auto" | "webSpeech" | "whisperWeb" | "whisperServer", - language: "auto", - mode: "toggle" as "toggle" | "hold", - autoSend: false, - whisperWebModel: "base" as "tiny" | "base" | "small", - }, -})); - -vi.mock("@/components/state-provider", () => ({ - useAppStore: ( - selector: (state: { userSettings: { voiceMode: typeof voicePrefs.value } }) => unknown, - ) => selector({ userSettings: { voiceMode: voicePrefs.value } }), -})); - -const transcribeAudio = vi.hoisted(() => vi.fn()); -vi.mock("@/lib/api/domains/voice-api", () => ({ transcribeAudio })); - -// ── Mock SpeechRecognition ───────────────────────────────────────────── - -type SpeechHandle = { - start: () => void; - stop: () => void; - abort: () => void; - onresult: ((ev: { resultIndex: number; results: unknown }) => void) | null; - onerror: ((ev: { error: string }) => void) | null; - onend: (() => void) | null; - continuous: boolean; - interimResults: boolean; - maxAlternatives: number; - lang: string; - startCalls: number; - stopCalls: number; - abortCalls: number; -}; - -let recognitionInstance: SpeechHandle | null = null; - -// Factory pattern instead of `class` so we can avoid aliasing `this` in the -// constructor (the lint rule disallows it) while still satisfying the -// `new ()` shape that useVoiceInput's `new Ctor()` calls. -function FakeSpeechRecognition() { - const handle: SpeechHandle = { - continuous: false, - interimResults: false, - maxAlternatives: 1, - lang: "", - onresult: null, - onerror: null, - onend: null, - startCalls: 0, - stopCalls: 0, - abortCalls: 0, - start() { - handle.startCalls += 1; - }, - stop() { - handle.stopCalls += 1; - }, - abort() { - handle.abortCalls += 1; - }, - }; - recognitionInstance = handle; - return handle; -} - -// Import after mocks so the module under test sees the mocked store. -import { useVoiceInput } from "./use-voice-input"; - -// ── Tests ─────────────────────────────────────────────────────────────── - -beforeEach(() => { - voicePrefs.value = { - engine: "auto", - language: "auto", - mode: "toggle", - autoSend: false, - whisperWebModel: "base", - }; - recognitionInstance = null; - transcribeAudio.mockReset(); - (window as unknown as { SpeechRecognition: unknown }).SpeechRecognition = - FakeSpeechRecognition as unknown as new () => SpeechHandle; - // MediaRecorder/getUserMedia not used in the auto→webSpeech path, but provide - // a stub so capability detection sees audioCapture available too. - (window as unknown as { MediaRecorder: { isTypeSupported: () => boolean } }).MediaRecorder = { - isTypeSupported: () => true, - }; - Object.defineProperty(global.navigator, "mediaDevices", { - value: { getUserMedia: vi.fn() }, - configurable: true, - }); -}); - -afterEach(() => { - delete (window as unknown as { SpeechRecognition?: unknown }).SpeechRecognition; - delete (window as unknown as { webkitSpeechRecognition?: unknown }).webkitSpeechRecognition; - delete (window as unknown as { MediaRecorder?: unknown }).MediaRecorder; -}); - -describe("useVoiceInput — Web Speech engine", () => { - it("reports supported and resolves engine = webSpeech under the default auto preference", () => { - const { result } = renderHook(() => useVoiceInput({ onTranscript: vi.fn() })); - expect(result.current.supported).toBe(true); - expect(result.current.engine).toBe("webSpeech"); - }); - - it("transitions idle → recording on start() and emits the final transcript on stop()", async () => { - const onTranscript = vi.fn(); - const { result } = renderHook(() => useVoiceInput({ onTranscript })); - - await act(async () => { - await result.current.start(); - }); - expect(result.current.state).toBe("recording"); - expect(recognitionInstance?.startCalls).toBe(1); - - act(() => { - recognitionInstance?.onresult?.({ - resultIndex: 0, - results: { - length: 1, - 0: { isFinal: true, length: 1, 0: { transcript: "hello world" } }, - } as unknown, - }); - recognitionInstance?.onend?.(); - }); - - await waitFor(() => { - expect(onTranscript).toHaveBeenCalledWith("hello world"); - expect(result.current.state).toBe("idle"); - }); - }); - - it("maps a not-allowed permission error to a permission-denied VoiceError", async () => { - const onError = vi.fn(); - const { result } = renderHook(() => useVoiceInput({ onTranscript: vi.fn(), onError })); - - await act(async () => { - await result.current.start(); - }); - act(() => { - recognitionInstance?.onerror?.({ error: "not-allowed" }); - }); - - expect(onError).toHaveBeenCalledWith({ - code: "permission-denied", - message: "Microphone permission denied.", - }); - expect(result.current.state).toBe("idle"); - }); -}); - -describe("useVoiceInput — capability gating", () => { - it("returns supported=false and engine=null when no engine is usable", () => { - delete (window as unknown as { SpeechRecognition?: unknown }).SpeechRecognition; - delete (window as unknown as { MediaRecorder?: unknown }).MediaRecorder; - Object.defineProperty(global.navigator, "mediaDevices", { value: {}, configurable: true }); - - const { result } = renderHook(() => useVoiceInput({ onTranscript: vi.fn() })); - expect(result.current.supported).toBe(false); - expect(result.current.engine).toBeNull(); - }); - - it("disables the hook entirely when enabled=false", () => { - const { result } = renderHook(() => useVoiceInput({ onTranscript: vi.fn(), enabled: false })); - expect(result.current.supported).toBe(false); - expect(result.current.engine).toBeNull(); - }); -}); - -describe("useVoiceInput — language preference", () => { - it("passes the pinned BCP-47 language to SpeechRecognition.lang", async () => { - voicePrefs.value = { ...voicePrefs.value, language: "pt-PT" }; - const { result } = renderHook(() => useVoiceInput({ onTranscript: vi.fn() })); - - await act(async () => { - await result.current.start(); - }); - expect(recognitionInstance?.lang).toBe("pt-PT"); - }); - - it("falls back to navigator.language when 'auto'", async () => { - voicePrefs.value = { ...voicePrefs.value, language: "auto" }; - Object.defineProperty(global.navigator, "language", { value: "fr-FR", configurable: true }); - const { result } = renderHook(() => useVoiceInput({ onTranscript: vi.fn() })); - await act(async () => { - await result.current.start(); - }); - expect(recognitionInstance?.lang).toBe("fr-FR"); - }); -}); diff --git a/apps/web/hooks/use-voice-input.ts b/apps/web/hooks/use-voice-input.ts deleted file mode 100644 index 454df30f8c..0000000000 --- a/apps/web/hooks/use-voice-input.ts +++ /dev/null @@ -1,493 +0,0 @@ -"use client"; - -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { ApiError } from "@/lib/api/client"; -import { transcribeAudio } from "@/lib/api/domains/voice-api"; -import { detectVoiceCapabilities, resolveActiveEngine } from "@/lib/voice/capabilities"; -import { WhisperWebClient, type WhisperWebProgress } from "@/lib/voice/whisper-web-client"; -import { useAppStore } from "@/components/state-provider"; -import type { VoiceInputEngine, WhisperWebModelSize } from "@/lib/types/http-voice"; - -// ── Public types ──────────────────────────────────────────────────────── - -export type VoiceInputState = "idle" | "requesting" | "recording" | "processing"; - -export type VoiceErrorCode = - | "permission-denied" - | "no-speech" - | "not-configured" - | "network" - | "unsupported" - | "model-load" - | "unknown"; - -export type VoiceError = { code: VoiceErrorCode; message: string }; - -export type VoiceModelLoadState = { - state: "idle" | "loading" | "ready" | "error"; - progress: number; -}; - -export type UseVoiceInputOptions = { - onTranscript: (text: string) => void; - onError?: (error: VoiceError) => void; - /** Set false to disable the hook entirely (e.g. for read-only contexts). */ - enabled?: boolean; -}; - -export type UseVoiceInputResult = { - supported: boolean; - engine: Exclude | null; - state: VoiceInputState; - error: VoiceError | null; - modelLoad: VoiceModelLoadState; - start: () => Promise; - stop: () => Promise; - cancel: () => void; -}; - -// ── Web Speech typings (DOM lib doesn't ship them) ───────────────────── - -type SpeechAlt = { transcript: string }; -type SpeechResult = { isFinal: boolean; 0: SpeechAlt; length: number }; -type SpeechResultList = { length: number; [index: number]: SpeechResult }; -type SpeechResultEvent = { resultIndex: number; results: SpeechResultList }; -type SpeechErrorEvent = { error: string; message?: string }; -type SpeechRecognitionInstance = { - lang: string; - continuous: boolean; - interimResults: boolean; - maxAlternatives: number; - start: () => void; - stop: () => void; - abort: () => void; - onresult: ((ev: SpeechResultEvent) => void) | null; - onerror: ((ev: SpeechErrorEvent) => void) | null; - onend: (() => void) | null; -}; - -type SpeechCtor = new () => SpeechRecognitionInstance; - -function createSpeechRecognition(): SpeechRecognitionInstance | null { - if (typeof window === "undefined") return null; - const w = window as Window & { - SpeechRecognition?: SpeechCtor; - webkitSpeechRecognition?: SpeechCtor; - }; - const Ctor = w.SpeechRecognition ?? w.webkitSpeechRecognition; - return Ctor ? new Ctor() : null; -} - -// ── Error mappers ─────────────────────────────────────────────────────── - -function mapSpeechError(code: string): VoiceError { - if (code === "not-allowed" || code === "service-not-allowed") { - return { code: "permission-denied", message: "Microphone permission denied." }; - } - if (code === "no-speech") return { code: "no-speech", message: "No speech detected. Try again." }; - if (code === "network") { - return { code: "network", message: "Voice recognition lost network connection." }; - } - if (code === "audio-capture") return { code: "unknown", message: "No microphone was found." }; - return { code: "unknown", message: `Voice recognition error: ${code}` }; -} - -function mapMicError(err: unknown): VoiceError { - if (err && typeof err === "object" && "name" in err) { - const name = (err as { name: string }).name; - if (name === "NotAllowedError" || name === "SecurityError") { - return { code: "permission-denied", message: "Microphone permission denied." }; - } - if (name === "NotFoundError" || name === "OverconstrainedError") { - return { code: "unknown", message: "No microphone was found." }; - } - } - return { code: "unknown", message: "Failed to start recording." }; -} - -function mapTranscribeError(err: unknown): VoiceError { - if (err instanceof ApiError && err.status === 503) { - return { - code: "not-configured", - message: - "Server-side transcription isn't configured. Pick Web Speech or Whisper Web in Voice Mode settings.", - }; - } - return { code: "network", message: "Transcription failed. Please try again." }; -} - -function whisperErrorMessage(err: unknown): VoiceError { - const message = err instanceof Error ? err.message : "Whisper Web failed to transcribe."; - return { code: "model-load", message }; -} - -function resolveLang(preference: string): string { - if (preference && preference !== "auto") return preference; - return typeof navigator !== "undefined" ? navigator.language : "en-US"; -} - -function resolveWhisperLang(preference: string): string | undefined { - if (!preference || preference === "auto") return undefined; - // Whisper's tokenizer only knows ISO 639-1 two-letter codes ("en", "pt"). - // The settings UI stores BCP-47 ("en-US", "pt-BR") so we can render - // human-friendly variant names — strip the region suffix here so the hint - // isn't silently dropped by the pipeline (which would then auto-detect and - // potentially pick the wrong dialect). - const dash = preference.indexOf("-"); - return dash > 0 ? preference.slice(0, dash).toLowerCase() : preference.toLowerCase(); -} - -// ── MediaRecorder capture primitive ───────────────────────────────────── - -function pickRecorderMime(): { mime: string; ext: string } { - if (typeof window === "undefined" || typeof window.MediaRecorder === "undefined") { - return { mime: "", ext: "webm" }; - } - const candidates: Array<{ mime: string; ext: string }> = [ - { mime: "audio/webm;codecs=opus", ext: "webm" }, - { mime: "audio/webm", ext: "webm" }, - { mime: "audio/mp4", ext: "m4a" }, - { mime: "audio/ogg;codecs=opus", ext: "ogg" }, - { mime: "audio/wav", ext: "wav" }, - ]; - for (const c of candidates) { - if (window.MediaRecorder.isTypeSupported(c.mime)) return c; - } - return { mime: "", ext: "webm" }; -} - -type CaptureHandle = { - stream: MediaStream; - recorder: MediaRecorder; - chunks: Blob[]; - mime: string; - ext: string; -}; - -async function startCapture(): Promise { - const { mime, ext } = pickRecorderMime(); - const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); - const recorder = new MediaRecorder(stream, mime ? { mimeType: mime } : undefined); - const chunks: Blob[] = []; - recorder.addEventListener("dataavailable", (e) => { - if (e.data && e.data.size > 0) chunks.push(e.data); - }); - recorder.start(); - return { stream, recorder, chunks, mime, ext }; -} - -function teardownCapture(handle: CaptureHandle | null) { - if (!handle) return; - for (const t of handle.stream.getTracks()) t.stop(); -} - -function stopCapture(handle: CaptureHandle): Promise { - return new Promise((resolve) => { - if (handle.recorder.state === "inactive") { - teardownCapture(handle); - resolve(null); - return; - } - handle.recorder.addEventListener( - "stop", - () => { - const type = handle.recorder.mimeType || handle.mime || "audio/webm"; - const blob = handle.chunks.length > 0 ? new Blob(handle.chunks, { type }) : null; - teardownCapture(handle); - resolve(blob); - }, - { once: true }, - ); - handle.recorder.stop(); - }); -} - -// ── Driver refs ───────────────────────────────────────────────────────── - -type ActiveDriverRef = - | { kind: "webSpeech"; recognition: SpeechRecognitionInstance } - | { kind: "capture"; handle: CaptureHandle; engine: "whisperWeb" | "whisperServer" } - | null; - -type DriverRefBox = { current: ActiveDriverRef }; -type WhisperRefBox = { current: WhisperWebClient | null }; - -function abortDriver(ref: DriverRefBox) { - const driver = ref.current; - if (!driver) return; - if (driver.kind === "webSpeech") { - // Detach callbacks before aborting so the trailing onerror/onend events - // that some browsers fire after .abort() don't sneak through and mutate - // hook state that the caller (cancel()) just reset. - driver.recognition.onresult = null; - driver.recognition.onerror = null; - driver.recognition.onend = null; - driver.recognition.abort(); - } else teardownCapture(driver.handle); - ref.current = null; -} - -// ── Web Speech driver ─────────────────────────────────────────────────── - -type WebSpeechHandlers = { - setState: (s: VoiceInputState) => void; - driverRef: DriverRefBox; - emitError: (e: VoiceError) => void; - onTranscriptRef: { current: (text: string) => void }; - lang: string; -}; - -function runWebSpeech(h: WebSpeechHandlers): void { - const recognition = createSpeechRecognition(); - if (!recognition) { - h.emitError({ code: "unsupported", message: "Voice recognition is not supported." }); - return; - } - const transcripts: string[] = []; - recognition.continuous = true; - recognition.interimResults = false; - recognition.maxAlternatives = 1; - recognition.lang = h.lang; - recognition.onresult = (ev) => { - for (let i = ev.resultIndex; i < ev.results.length; i++) { - const r = ev.results[i]; - if (r.isFinal && r[0]?.transcript) transcripts.push(r[0].transcript.trim()); - } - }; - recognition.onerror = (ev) => h.emitError(mapSpeechError(ev.error)); - recognition.onend = () => { - h.driverRef.current = null; - h.setState("idle"); - const joined = transcripts.join(" ").trim(); - if (joined) h.onTranscriptRef.current(joined); - }; - try { - recognition.start(); - h.driverRef.current = { kind: "webSpeech", recognition }; - h.setState("recording"); - } catch { - h.emitError({ code: "unknown", message: "Failed to start voice recognition." }); - } -} - -// ── Capture engines (whisperWeb + whisperServer) ─────────────────────── - -type CaptureHandlers = { - setState: (s: VoiceInputState) => void; - emitError: (e: VoiceError) => void; - driverRef: DriverRefBox; -}; - -async function beginCapture( - which: "whisperWeb" | "whisperServer", - h: CaptureHandlers, -): Promise { - h.setState("requesting"); - try { - const handle = await startCapture(); - h.driverRef.current = { kind: "capture", handle, engine: which }; - h.setState("recording"); - } catch (err) { - h.emitError(mapMicError(err)); - } -} - -type FinishCaptureHandlers = { - driverRef: DriverRefBox; - whisperRef: WhisperRefBox; - setState: (s: VoiceInputState) => void; - setModelLoad: (next: VoiceModelLoadState) => void; - emitError: (e: VoiceError) => void; - onTranscriptRef: { current: (text: string) => void }; - whisperModel: WhisperWebModelSize; - language: string; -}; - -async function finishCapture(h: FinishCaptureHandlers): Promise { - const driver = h.driverRef.current; - if (!driver || driver.kind !== "capture") return; - // Claim the driver synchronously *before* the first await. In hold mode, - // pointerup + pointerleave both fire in the same task and both call stop(); - // without this early null, the second invocation would also enter - // finishCapture, race the first, and could clobber a brand-new recording's - // driverRef if the user re-triggered between them. - h.driverRef.current = null; - h.setState("processing"); - const blob = await stopCapture(driver.handle); - if (!blob) { - h.setState("idle"); - return; - } - try { - const text = - driver.engine === "whisperServer" - ? await transcribeViaServer(blob, driver.handle.ext) - : await transcribeViaWhisperWeb(blob, h); - if (text) h.onTranscriptRef.current(text); - h.setState("idle"); - } catch (err) { - if (driver.engine === "whisperServer") h.emitError(mapTranscribeError(err)); - else h.emitError(whisperErrorMessage(err)); - } -} - -async function transcribeViaServer(blob: Blob, ext: string): Promise { - const result = await transcribeAudio(blob, `recording.${ext}`); - return result.text.trim(); -} - -async function transcribeViaWhisperWeb(blob: Blob, h: FinishCaptureHandlers): Promise { - const client = await ensureWhisperClient(h); - const text = await client.transcribe(blob, resolveWhisperLang(h.language)); - return text.trim(); -} - -async function ensureWhisperClient(h: FinishCaptureHandlers): Promise { - if (!h.whisperRef.current) { - h.whisperRef.current = new WhisperWebClient({ - onProgress: (p: WhisperWebProgress) => - // transformers.js emits progress on a 0–100 scale, but the rest of the - // pipeline (and the button's `* 100` display) treats `modelLoad.progress` - // as a 0–1 fraction (matching the `ready: 1` convention below). Normalise - // here so the button doesn't render "5000%" mid-download. - h.setModelLoad({ state: "loading", progress: p.progress / 100 }), - }); - h.setModelLoad({ state: "loading", progress: 0 }); - } - try { - await h.whisperRef.current.init(h.whisperModel); - h.setModelLoad({ state: "ready", progress: 1 }); - } catch (err) { - h.setModelLoad({ state: "error", progress: 0 }); - throw err; - } - return h.whisperRef.current; -} - -// ── Hook helpers ──────────────────────────────────────────────────────── - -function useVoiceModePrefs() { - return useAppStore((s) => s.userSettings.voiceMode); -} - -function useCallbackRefs(opts: UseVoiceInputOptions) { - const onTranscriptRef = useRef(opts.onTranscript); - const onErrorRef = useRef(opts.onError); - useEffect(() => { - onTranscriptRef.current = opts.onTranscript; - onErrorRef.current = opts.onError; - }); - return { onTranscriptRef, onErrorRef }; -} - -// Re-init the whisper client whenever the user switches model size, so we -// don't keep an old in-memory model around when the next start() runs. -function useDisposeWhisperOnModelChange( - whisperRef: WhisperRefBox, - modelSize: string, - reset: () => void, -) { - const previousModelRef = useRef(modelSize); - useEffect(() => { - if (previousModelRef.current === modelSize) return; - previousModelRef.current = modelSize; - whisperRef.current?.dispose(); - whisperRef.current = null; - reset(); - }, [modelSize, whisperRef, reset]); -} - -function useUnmountCleanup(driverRef: DriverRefBox, whisperRef: WhisperRefBox) { - useEffect(() => { - return () => { - abortDriver(driverRef); - whisperRef.current?.dispose(); - whisperRef.current = null; - }; - }, [driverRef, whisperRef]); -} - -// ── Hook ──────────────────────────────────────────────────────────────── - -export function useVoiceInput(opts: UseVoiceInputOptions): UseVoiceInputResult { - const caps = useMemo(() => detectVoiceCapabilities(), []); - const prefs = useVoiceModePrefs(); - const enabled = opts.enabled !== false; - const engine = useMemo( - () => (enabled ? resolveActiveEngine(prefs.engine, caps, true) : null), - [enabled, prefs.engine, caps], - ); - const supported = engine !== null; - - const [state, setState] = useState("idle"); - const [error, setError] = useState(null); - const [modelLoad, setModelLoad] = useState({ - state: "idle", - progress: 0, - }); - - const driverRef = useRef(null); - const whisperRef = useRef(null); - const { onTranscriptRef, onErrorRef } = useCallbackRefs(opts); - - const emitError = useCallback( - (e: VoiceError) => { - setError(e); - setState("idle"); - onErrorRef.current?.(e); - }, - [onErrorRef], - ); - - const resetModelLoad = useCallback(() => setModelLoad({ state: "idle", progress: 0 }), []); - - useUnmountCleanup(driverRef, whisperRef); - useDisposeWhisperOnModelChange(whisperRef, prefs.whisperWebModel, resetModelLoad); - - const start = useCallback(async () => { - if (!supported || !engine) { - emitError({ code: "unsupported", message: "Voice input is not supported in this browser." }); - return; - } - if (state !== "idle") return; - setError(null); - if (engine === "webSpeech") { - runWebSpeech({ - setState, - driverRef, - emitError, - onTranscriptRef, - lang: resolveLang(prefs.language), - }); - return; - } - await beginCapture(engine, { setState, emitError, driverRef }); - }, [supported, engine, state, emitError, prefs.language, onTranscriptRef]); - - const stop = useCallback(async () => { - const driver = driverRef.current; - if (!driver) return; - if (driver.kind === "webSpeech") { - driver.recognition.stop(); - return; - } - await finishCapture({ - driverRef, - whisperRef, - setState, - setModelLoad, - emitError, - onTranscriptRef, - whisperModel: prefs.whisperWebModel, - language: prefs.language, - }); - }, [emitError, prefs.whisperWebModel, prefs.language, onTranscriptRef]); - - const cancel = useCallback(() => { - abortDriver(driverRef); - setState("idle"); - setError(null); - }, []); - - return { supported, engine, state, error, modelLoad, start, stop, cancel }; -} diff --git a/apps/web/lib/api/domains/plugins-api.ts b/apps/web/lib/api/domains/plugins-api.ts index 781b98d146..5a6574a1b6 100644 --- a/apps/web/lib/api/domains/plugins-api.ts +++ b/apps/web/lib/api/domains/plugins-api.ts @@ -60,8 +60,10 @@ export async function installPluginUpload( const formData = new FormData(); formData.append("package", file); - // Spread caller init *first* so method/body always win, matching the - // convention in lib/api/domains/voice-api.ts. + // Do NOT set Content-Type: the browser picks multipart/form-data with the + // right boundary for a FormData body. Spread caller init *first* so + // method/body always win, or a caller passing a stale method or body would + // silently break the upload. const response = await fetch(`${baseUrl}${BASE}/install`, { ...options?.init, method: "POST", diff --git a/apps/web/lib/api/domains/voice-api.test.ts b/apps/web/lib/api/domains/voice-api.test.ts deleted file mode 100644 index d3618cae83..0000000000 --- a/apps/web/lib/api/domains/voice-api.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { ApiError } from "../client"; -import { transcribeAudio } from "./voice-api"; - -const originalFetch = global.fetch; - -describe("transcribeAudio", () => { - afterEach(() => { - global.fetch = originalFetch; - }); - - beforeEach(() => { - vi.restoreAllMocks(); - }); - - it("posts multipart/form-data with the audio under the 'audio' field", async () => { - let capturedRequest: { method?: string; bodyText: string } = { bodyText: "" }; - global.fetch = vi.fn(async (_url: RequestInfo | URL, init?: RequestInit) => { - capturedRequest = { - method: init?.method, - bodyText: init?.body instanceof FormData ? "" : String(init?.body), - }; - return new Response(JSON.stringify({ text: "hi" }), { - status: 200, - headers: { "Content-Type": "application/json" }, - }); - }) as unknown as typeof fetch; - - const blob = new Blob([new Uint8Array([1, 2, 3])], { type: "audio/webm" }); - const result = await transcribeAudio(blob, "clip.webm", { - baseUrl: "http://example.test", - }); - - expect(result.text).toBe("hi"); - expect(capturedRequest.method).toBe("POST"); - expect(capturedRequest.bodyText).toBe(""); - }); - - it("throws ApiError(503) when the server reports not-configured", async () => { - global.fetch = vi.fn( - async () => - new Response(JSON.stringify({ error: "voice transcription is not configured" }), { - status: 503, - }), - ) as unknown as typeof fetch; - - const blob = new Blob([new Uint8Array([1])], { type: "audio/webm" }); - await expect(transcribeAudio(blob, "x.webm", { baseUrl: "http://x" })).rejects.toMatchObject({ - status: 503, - }); - }); - - it("surfaces non-2xx errors as ApiError instances", async () => { - global.fetch = vi.fn( - async () => new Response("bad", { status: 502, statusText: "Bad Gateway" }), - ) as unknown as typeof fetch; - - const blob = new Blob([new Uint8Array([1])], { type: "audio/webm" }); - await expect(transcribeAudio(blob, "x.webm", { baseUrl: "http://x" })).rejects.toBeInstanceOf( - ApiError, - ); - }); -}); diff --git a/apps/web/lib/api/domains/voice-api.ts b/apps/web/lib/api/domains/voice-api.ts deleted file mode 100644 index d3af1a571a..0000000000 --- a/apps/web/lib/api/domains/voice-api.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { ApiError, type ApiRequestOptions } from "../client"; -import { getBackendConfig } from "@/lib/config"; - -export type TranscribeResponse = { - text: string; -}; - -/** - * POST audio to the backend Whisper fallback. Returns the transcribed text. - * - * Throws ApiError on non-2xx. Two statuses are meaningful to the caller: - * - 503: server has no API key configured — the hook should treat the - * Whisper fallback as unavailable and surface a clean message. - * - any other non-2xx: transient error — show a generic toast. - */ -export async function transcribeAudio( - blob: Blob, - filename: string, - options?: ApiRequestOptions, -): Promise { - const baseUrl = options?.baseUrl ?? getBackendConfig().apiBaseUrl; - const formData = new FormData(); - formData.append("audio", blob, filename); - - // Do NOT set Content-Type: the browser sets multipart/form-data with the - // correct boundary automatically when given a FormData body. Spread caller - // init *first* so method/body always win — otherwise a caller passing - // `init: { method: "GET" }` (or a stale body) would silently break the upload. - const response = await fetch(`${baseUrl}/api/v1/transcribe`, { - ...options?.init, - method: "POST", - body: formData, - }); - - if (!response.ok) { - let body: unknown = null; - try { - body = await response.json(); - } catch { - // body remains null - } - let message = `Transcription failed: ${response.status} ${response.statusText}`; - if (body && typeof body === "object" && "error" in body) { - const errVal = (body as { error?: unknown }).error; - if (typeof errVal === "string") message = errVal; - } - throw new ApiError(message, response.status, body); - } - - return (await response.json()) as TranscribeResponse; -} diff --git a/apps/web/lib/keyboard/constants.ts b/apps/web/lib/keyboard/constants.ts index 5a1d7a449b..5617fab2b8 100644 --- a/apps/web/lib/keyboard/constants.ts +++ b/apps/web/lib/keyboard/constants.ts @@ -167,12 +167,6 @@ export const SHORTCUTS = { key: KEYS.F, modifiers: { ctrlOrCmd: true }, }, - // Cmd+Shift+M starts/stops voice input on the chat composer. The default - // is configurable per-user via the Voice Mode settings page. - VOICE_INPUT_TOGGLE: { - key: KEYS.M, - modifiers: { ctrlOrCmd: true, shift: true }, - }, // Ctrl+R opens the bash-style reverse-i-search overlay on the chat composer. // Defaults to plain Ctrl (not ctrlOrCmd) so the Mac browser refresh shortcut // Cmd+R keeps working out of the box. diff --git a/apps/web/lib/keyboard/shortcut-overrides.test.ts b/apps/web/lib/keyboard/shortcut-overrides.test.ts index d30061e8f6..bcb349c3e9 100644 --- a/apps/web/lib/keyboard/shortcut-overrides.test.ts +++ b/apps/web/lib/keyboard/shortcut-overrides.test.ts @@ -25,11 +25,10 @@ describe("CONFIGURABLE_SHORTCUTS", () => { expect(ids).toContain("TOGGLE_PLAN_MODE"); expect(ids).toContain("TASK_SWITCHER"); expect(ids).toContain("TASK_SWITCHER_REVERSE"); - expect(ids).toContain("VOICE_INPUT_TOGGLE"); expect(ids).toContain("REVERSE_SEARCH"); expect(ids).toContain("OPEN_TASK_PR"); expect(ids).toContain("WORKSPACE_PICKER"); - expect(ids).toHaveLength(17); + expect(ids).toHaveLength(16); }); it("each entry has a label and default matching SHORTCUTS", () => { diff --git a/apps/web/lib/keyboard/shortcut-overrides.ts b/apps/web/lib/keyboard/shortcut-overrides.ts index 06660d9dd2..77aa5263d1 100644 --- a/apps/web/lib/keyboard/shortcut-overrides.ts +++ b/apps/web/lib/keyboard/shortcut-overrides.ts @@ -14,7 +14,6 @@ export type ConfigurableShortcutId = | "TOGGLE_PLAN_MODE" | "TASK_SWITCHER" | "TASK_SWITCHER_REVERSE" - | "VOICE_INPUT_TOGGLE" | "REVERSE_SEARCH" | "OPEN_TASK_PR" | "WORKSPACE_PICKER"; @@ -58,7 +57,6 @@ export const CONFIGURABLE_SHORTCUTS: Record< label: "Recent Task Switcher (Backward)", default: SHORTCUTS.TASK_SWITCHER_REVERSE, }, - VOICE_INPUT_TOGGLE: { label: "Voice Input", default: SHORTCUTS.VOICE_INPUT_TOGGLE }, REVERSE_SEARCH: { label: "Reverse Chat Search", default: SHORTCUTS.REVERSE_SEARCH }, OPEN_TASK_PR: { label: "Open Task Pull Request", default: SHORTCUTS.OPEN_TASK_PR }, WORKSPACE_PICKER: { label: "Open Workspace Picker", default: SHORTCUTS.WORKSPACE_PICKER }, diff --git a/apps/web/lib/settings-discovery/catalog.test.ts b/apps/web/lib/settings-discovery/catalog.test.ts index b45830aa12..0ec990471c 100644 --- a/apps/web/lib/settings-discovery/catalog.test.ts +++ b/apps/web/lib/settings-discovery/catalog.test.ts @@ -22,9 +22,6 @@ const STABLE_CONTROL_IDS = [ "layouts-profiles", "sprites-connection", "sprites-instances", - "voice-enable", - "voice-engine", - "voice-behavior", "utility-default-model", "utility-actions", "external-mcp-endpoints", @@ -68,17 +65,13 @@ describe("settings discovery catalog invariants", () => { const ids = SETTINGS_DISCOVERY_DEFINITIONS.map((entry) => entry.id); expect(ids).toEqual(expect.arrayContaining([...STABLE_CONTROL_IDS])); + }); - const voiceEntries = SETTINGS_DISCOVERY_DEFINITIONS.filter((entry) => - entry.id.startsWith("voice-"), - ); - expect(voiceEntries).toHaveLength(6); - expect(new Set(voiceEntries.map((entry) => entry.href))).toEqual( - new Set(["/settings/preferences/task-behavior"]), - ); - expect(new Set(voiceEntries.map((entry) => entry.groupId))).toEqual(new Set(["preferences"])); - expect(voiceEntries.find((entry) => entry.id === "voice-mode")?.targetId).toBe( - "setting-voice-mode", + it("no longer indexes the removed core Voice Mode controls", () => { + // Voice Mode moved to a plugin; its settings live on the plugin's own + // page and must not reappear in the core discovery catalog. + expect(SETTINGS_DISCOVERY_DEFINITIONS.filter((entry) => entry.id.startsWith("voice"))).toEqual( + [], ); }); diff --git a/apps/web/lib/settings-discovery/catalog/preferences.ts b/apps/web/lib/settings-discovery/catalog/preferences.ts index 3111901b16..5eda45cc49 100644 --- a/apps/web/lib/settings-discovery/catalog/preferences.ts +++ b/apps/web/lib/settings-discovery/catalog/preferences.ts @@ -13,7 +13,6 @@ const NOTIFICATIONS_ID = "preferences-notifications"; export const NOTIFICATIONS_SETTINGS_HREF = `${PREFERENCES_SETTINGS_HREF}/notifications`; export const KEYBOARD_SHORTCUTS_SETTINGS_HREF = `${PREFERENCES_SETTINGS_HREF}/keyboard-shortcuts`; const TASK_BEHAVIOR_ID = "preferences-task-behavior"; -const VOICE_MODE_DISCOVERY_ID = "voice-mode"; export const TASK_BEHAVIOR_SETTINGS_HREF = `${PREFERENCES_SETTINGS_HREF}/task-behavior`; export const GENERAL_SETTINGS_TARGETS = { colorTheme: "setting-color-theme", @@ -39,12 +38,6 @@ export const GENERAL_SETTINGS_TARGETS = { unreadMessages: "setting-unread-messages", transcriptNavigation: "setting-transcript-navigation", messageQueue: "setting-message-queue", - voiceMode: "setting-voice-mode", - voiceEnable: "setting-voice-enable", - voiceEngine: "setting-voice-engine", - voiceBehavior: "setting-voice-behavior", - voiceModel: "setting-voice-model", - voiceShortcut: "setting-voice-shortcut", spritesConnection: "setting-sprites-connection", spritesInstances: "setting-sprites-instances", } as const; @@ -377,68 +370,4 @@ export const PREFERENCES_DISCOVERY_DEFINITIONS: SettingsDiscoveryDefinition[] = targetId: GENERAL_SETTINGS_TARGETS.messageQueue, order: 66, }, - { - id: VOICE_MODE_DISCOVERY_ID, - kind: "section", - labelKey: "settings:voiceMode", - parentId: TASK_BEHAVIOR_ID, - groupId: "preferences", - href: TASK_BEHAVIOR_SETTINGS_HREF, - targetId: GENERAL_SETTINGS_TARGETS.voiceMode, - order: 67, - }, - { - id: "voice-enable", - kind: "control", - labelKey: "settings:voiceEnableTitle", - aliasesKey: "settings:discoveryAliasesVoiceEnable", - parentId: VOICE_MODE_DISCOVERY_ID, - groupId: "preferences", - href: TASK_BEHAVIOR_SETTINGS_HREF, - targetId: GENERAL_SETTINGS_TARGETS.voiceEnable, - order: 68, - }, - { - id: "voice-engine", - kind: "control", - labelKey: "settings:voiceEngineTitle", - aliasesKey: "settings:discoveryAliasesVoiceEngine", - parentId: VOICE_MODE_DISCOVERY_ID, - groupId: "preferences", - href: TASK_BEHAVIOR_SETTINGS_HREF, - targetId: GENERAL_SETTINGS_TARGETS.voiceEngine, - order: 69, - }, - { - id: "voice-behavior", - kind: "control", - labelKey: "settings:voiceBehavior", - aliasesKey: "settings:discoveryAliasesVoiceBehavior", - parentId: VOICE_MODE_DISCOVERY_ID, - groupId: "preferences", - href: TASK_BEHAVIOR_SETTINGS_HREF, - targetId: GENERAL_SETTINGS_TARGETS.voiceBehavior, - order: 70, - }, - { - id: "voice-model", - kind: "control", - labelKey: "settings:voiceWhisperModelTitle", - parentId: VOICE_MODE_DISCOVERY_ID, - groupId: "preferences", - href: TASK_BEHAVIOR_SETTINGS_HREF, - targetId: GENERAL_SETTINGS_TARGETS.voiceModel, - order: 71, - }, - { - id: "voice-shortcut", - kind: "control", - labelKey: "settings:discoveryVoiceShortcut", - aliasesKey: "settings:discoveryAliasesVoiceShortcut", - parentId: VOICE_MODE_DISCOVERY_ID, - groupId: "preferences", - href: TASK_BEHAVIOR_SETTINGS_HREF, - targetId: GENERAL_SETTINGS_TARGETS.voiceShortcut, - order: 72, - }, ]; diff --git a/apps/web/lib/ssr/user-settings.test.ts b/apps/web/lib/ssr/user-settings.test.ts index 33cb006e54..1080ffac4d 100644 --- a/apps/web/lib/ssr/user-settings.test.ts +++ b/apps/web/lib/ssr/user-settings.test.ts @@ -6,7 +6,6 @@ import { parseLspStatusLocation, parseStartupPage, parseSystemMetricsDisplay, - parseVoiceMode, } from "./user-settings"; import { compareUserSettingsRevisions } from "@/lib/settings/user-settings-revision"; import { workspaceId as toWorkspaceId } from "@/lib/types/ids"; @@ -529,81 +528,6 @@ describe("parseChangesPanelLayout", () => { }); }); -describe("parseVoiceMode", () => { - it("maps every field from the snake_case wire payload", () => { - expect( - parseVoiceMode({ - enabled: false, - engine: "whisperWeb", - language: "pt-PT", - mode: "hold", - auto_send: true, - whisper_web_model: "small", - }), - ).toEqual({ - enabled: false, - engine: "whisperWeb", - language: "pt-PT", - mode: "hold", - autoSend: true, - whisperWebModel: "small", - }); - }); - - it("returns the defaults when the payload is undefined", () => { - expect(parseVoiceMode(undefined)).toEqual({ - enabled: true, - engine: "auto", - language: "auto", - mode: "toggle", - autoSend: false, - whisperWebModel: "base", - }); - }); - - it("defaults enabled to true when the wire payload omits the field (old rows)", () => { - const result = parseVoiceMode({ - engine: "auto", - language: "auto", - mode: "toggle", - auto_send: false, - whisper_web_model: "base", - } as unknown as Parameters[0]); - expect(result.enabled).toBe(true); - }); - - it("fills in defaults for missing string fields and coerces auto_send to false", () => { - const result = parseVoiceMode({ - engine: "" as unknown as "auto", - language: "", - mode: "" as unknown as "toggle", - whisper_web_model: "" as unknown as "base", - } as unknown as Parameters[0]); - expect(result).toEqual({ - enabled: true, - engine: "auto", - language: "auto", - mode: "toggle", - autoSend: false, - whisperWebModel: "base", - }); - }); -}); - -describe("mapUserSettingsResponse voice mode", () => { - it("defaults the whole voiceMode object when response is null", () => { - const result = mapUserSettingsResponse(null); - expect(result.voiceMode).toEqual({ - enabled: true, - engine: "auto", - language: "auto", - mode: "toggle", - autoSend: false, - whisperWebModel: "base", - }); - }); -}); - describe("prevent auto-start on open preference", () => { it("defaults the missing preference to false", () => { expect(buildCoreFields({}).preventAutoStartAgentOnOpen).toBe(false); diff --git a/apps/web/lib/ssr/user-settings.ts b/apps/web/lib/ssr/user-settings.ts index ea85fba636..9c60bfc8f2 100644 --- a/apps/web/lib/ssr/user-settings.ts +++ b/apps/web/lib/ssr/user-settings.ts @@ -6,18 +6,13 @@ import { } from "@/lib/tasks/tasks-list-options"; import { fromApiSidebarDraft, fromApiSidebarView } from "@/lib/state/slices/ui/sidebar-view-wire"; import type { SidebarView, SidebarViewDraft } from "@/lib/state/slices/ui/sidebar-view-types"; -import { - DEFAULT_VOICE_MODE_STATE, - type UserSettingsState, - type VoiceModeState, -} from "@/lib/state/slices/settings/types"; +import { type UserSettingsState } from "@/lib/state/slices/settings/types"; import type { SidebarTaskPrefsApi, UserSettings, UserSettingsResponse } from "@/lib/types/http"; import type { LspStatusLocation, MCPTaskAgentProfileDefault, StartupPage, } from "@/lib/types/http-user-settings"; -import type { VoiceModeSettings } from "@/lib/types/http-voice"; export type UserSettingsData = Omit, "workspace_id"> & { workspace_id?: string; @@ -85,7 +80,6 @@ export function createDefaultUserSettings(): UserSettingsState { systemMetricsDisplay: { showInTopbar: false, simplified: false }, appStatusBarEnabled: false, appStatusBarOrder: { leftItemIds: [], rightItemIds: [] }, - voiceMode: { ...DEFAULT_VOICE_MODE_STATE }, hiddenWorkflowStepIds: {}, loaded: false, }; @@ -127,25 +121,6 @@ export function parseAppStatusBarOrder(value: UserSettingsData["app_status_bar_o }; } -/** - * Maps the backend's snake_case VoiceMode payload into the camelCase shape - * the store and UI use. Missing or partial payloads fall back to the defaults - * so an old user row (written before VoiceMode existed) doesn't surface as - * an empty string the radio groups can't render. `enabled` defaults to true - * for users who haven't toggled it — voice mode is opt-out, not opt-in. - */ -export function parseVoiceMode(value: VoiceModeSettings | undefined): VoiceModeState { - if (!value) return { ...DEFAULT_VOICE_MODE_STATE }; - return { - enabled: typeof value.enabled === "boolean" ? value.enabled : true, - engine: value.engine || DEFAULT_VOICE_MODE_STATE.engine, - language: value.language || DEFAULT_VOICE_MODE_STATE.language, - mode: value.mode || DEFAULT_VOICE_MODE_STATE.mode, - autoSend: typeof value.auto_send === "boolean" ? value.auto_send : false, - whisperWebModel: value.whisper_web_model || DEFAULT_VOICE_MODE_STATE.whisperWebModel, - }; -} - function buildTerminalFields(s: UserSettingsData, current: UserSettingsState) { return { terminalLinkBehavior: @@ -165,12 +140,6 @@ function buildTerminalFields(s: UserSettingsData, current: UserSettingsState) { }; } -function buildVoiceModeFields(s: UserSettingsData, current: UserSettingsState) { - return { - voiceMode: s.voice_mode === undefined ? current.voiceMode : parseVoiceMode(s.voice_mode), - }; -} - function buildSystemMetricsDisplayFields( s: UserSettingsData | undefined, current: UserSettingsState, @@ -340,7 +309,6 @@ export function buildCoreFields( hiddenWorkflowStepIds: s.kanban_hidden_step_ids ?? current.hiddenWorkflowStepIds, ...buildTerminalFields(s, current), ...buildSystemMetricsDisplayFields(s, current), - ...buildVoiceModeFields(s, current), }; } diff --git a/apps/web/lib/state/slices/settings/types.ts b/apps/web/lib/state/slices/settings/types.ts index 47178ae8e6..a1f9239b7b 100644 --- a/apps/web/lib/state/slices/settings/types.ts +++ b/apps/web/lib/state/slices/settings/types.ts @@ -14,11 +14,6 @@ import type { MCPTaskAgentProfileDefault, StartupPage, } from "@/lib/types/http"; -import type { - VoiceInputActivationMode, - VoiceInputEngine, - WhisperWebModelSize, -} from "@/lib/types/http-voice"; import type { SidebarView, SidebarViewDraft } from "@/lib/state/slices/ui/sidebar-view-types"; import type { SidebarTaskPrefsState } from "@/lib/state/slices/ui/types"; import type { SecretListItem } from "@/lib/types/http-secrets"; @@ -305,7 +300,6 @@ export type UserSettingsState = { systemMetricsDisplay: { showInTopbar: boolean; simplified: boolean }; appStatusBarEnabled: boolean; appStatusBarOrder: AppStatusBarOrderState; - voiceMode: VoiceModeState; hiddenWorkflowStepIds: Record; loaded: boolean; }; @@ -324,25 +318,6 @@ export type TaskCreateLastUsedState = { synced?: boolean; }; -export type VoiceModeState = { - enabled: boolean; - engine: VoiceInputEngine; - language: string; - mode: VoiceInputActivationMode; - autoSend: boolean; - whisperWebModel: WhisperWebModelSize; -}; - -/** Default values used by the slice init and by SSR hydration fallback. */ -export const DEFAULT_VOICE_MODE_STATE: VoiceModeState = { - enabled: true, - engine: "auto", - language: "auto", - mode: "toggle", - autoSend: false, - whisperWebModel: "base", -}; - export type SettingsSliceState = { executors: ExecutorsState; settingsAgents: SettingsAgentsState; diff --git a/apps/web/lib/types/http-user-settings.ts b/apps/web/lib/types/http-user-settings.ts index fc2ea1d6a7..b7ee5e72cd 100644 --- a/apps/web/lib/types/http-user-settings.ts +++ b/apps/web/lib/types/http-user-settings.ts @@ -1,5 +1,4 @@ import type { WorkspaceId } from "./ids"; -import type { VoiceModeSettings } from "./http-voice"; export type MCPTaskAgentProfileDefault = "current_task" | "workspace_default"; export type StartupPage = "task_overview" | "last_task"; @@ -104,7 +103,6 @@ export type UserSettings = { system_metrics_display?: { show_in_topbar?: boolean; simplified?: boolean }; app_status_bar_enabled?: boolean; app_status_bar_order?: AppStatusBarOrderApi; - voice_mode?: VoiceModeSettings; kanban_hidden_step_ids?: Record; revision?: number; updated_at: string; @@ -169,6 +167,5 @@ export type UserSettingsUpdatePayload = { system_metrics_display?: { show_in_topbar?: boolean; simplified?: boolean }; app_status_bar_enabled?: boolean; app_status_bar_order?: AppStatusBarOrderApi; - voice_mode?: VoiceModeSettings; kanban_hidden_step_ids?: Record; }; diff --git a/apps/web/lib/types/http-voice.ts b/apps/web/lib/types/http-voice.ts deleted file mode 100644 index c433515247..0000000000 --- a/apps/web/lib/types/http-voice.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Wire types for the Voice Mode user settings. Kept in their own module so - * http.ts stays under the 600-line file limit. - */ - -export type VoiceInputEngine = "auto" | "webSpeech" | "whisperWeb" | "whisperServer"; -export type VoiceInputActivationMode = "toggle" | "hold"; -export type WhisperWebModelSize = "tiny" | "base" | "small"; - -export type VoiceModeSettings = { - enabled: boolean; - engine: VoiceInputEngine; - language: string; - mode: VoiceInputActivationMode; - auto_send: boolean; - whisper_web_model: WhisperWebModelSize; -}; diff --git a/apps/web/lib/voice/capabilities.test.ts b/apps/web/lib/voice/capabilities.test.ts deleted file mode 100644 index d8b8d71910..0000000000 --- a/apps/web/lib/voice/capabilities.test.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { describe, it, expect, afterEach, vi } from "vitest"; -import { detectVoiceCapabilities, resolveActiveEngine } from "./capabilities"; - -describe("detectVoiceCapabilities", () => { - afterEach(() => { - vi.unstubAllGlobals(); - delete (window as unknown as { SpeechRecognition?: unknown }).SpeechRecognition; - delete (window as unknown as { webkitSpeechRecognition?: unknown }).webkitSpeechRecognition; - delete (window as unknown as { MediaRecorder?: unknown }).MediaRecorder; - }); - - it("reports webSpeech true when window.SpeechRecognition exists", () => { - (window as unknown as { SpeechRecognition: () => void }).SpeechRecognition = () => {}; - expect(detectVoiceCapabilities().webSpeech).toBe(true); - }); - - it("reports webSpeech true on the prefixed webkit variant too", () => { - (window as unknown as { webkitSpeechRecognition: () => void }).webkitSpeechRecognition = - () => {}; - expect(detectVoiceCapabilities().webSpeech).toBe(true); - }); - - it("reports audioCapture true when MediaRecorder + getUserMedia are present", () => { - (window as unknown as { MediaRecorder: object }).MediaRecorder = { - isTypeSupported: () => true, - }; - vi.stubGlobal("navigator", { mediaDevices: { getUserMedia: () => Promise.resolve({}) } }); - expect(detectVoiceCapabilities().audioCapture).toBe(true); - }); - - it("reports everything false when no APIs are available", () => { - vi.stubGlobal("navigator", {}); - expect(detectVoiceCapabilities()).toEqual({ - webSpeech: false, - whisperWeb: false, - audioCapture: false, - }); - }); -}); - -describe("resolveActiveEngine", () => { - const allAvailable = { webSpeech: true, whisperWeb: true, audioCapture: true }; - - it("auto picks webSpeech first when available", () => { - expect(resolveActiveEngine("auto", allAvailable, true)).toBe("webSpeech"); - }); - - it("auto falls back to whisperWeb when webSpeech is missing", () => { - expect( - resolveActiveEngine("auto", { webSpeech: false, whisperWeb: true, audioCapture: true }, true), - ).toBe("whisperWeb"); - }); - - it("auto falls back to whisperServer when no in-browser engine is available", () => { - expect( - resolveActiveEngine( - "auto", - { webSpeech: false, whisperWeb: false, audioCapture: true }, - true, - ), - ).toBe("whisperServer"); - }); - - it("returns null when nothing is usable", () => { - expect( - resolveActiveEngine( - "auto", - { webSpeech: false, whisperWeb: false, audioCapture: false }, - true, - ), - ).toBeNull(); - }); - - it("honors a pinned engine when usable", () => { - expect(resolveActiveEngine("whisperWeb", allAvailable, true)).toBe("whisperWeb"); - }); - - it("falls back along the auto order when the pinned engine is missing", () => { - expect( - resolveActiveEngine( - "whisperWeb", - { webSpeech: true, whisperWeb: false, audioCapture: true }, - true, - ), - ).toBe("webSpeech"); - }); - - it("treats whisperServer as unusable when serverFallbackEnabled is false", () => { - expect( - resolveActiveEngine( - "whisperServer", - { webSpeech: false, whisperWeb: false, audioCapture: true }, - false, - ), - ).toBeNull(); - }); -}); diff --git a/apps/web/lib/voice/capabilities.ts b/apps/web/lib/voice/capabilities.ts deleted file mode 100644 index 6fd36f1617..0000000000 --- a/apps/web/lib/voice/capabilities.ts +++ /dev/null @@ -1,75 +0,0 @@ -"use client"; - -import type { VoiceInputEngine } from "@/lib/types/http-voice"; - -/** - * Capability report for the voice-mode engines available in the current - * browser. Shared between `useVoiceInput` (which picks the active engine) - * and the Voice Mode settings page (which decides which options to render). - */ -export type VoiceCapabilities = { - webSpeech: boolean; - whisperWeb: boolean; - /** True if the browser supports MediaRecorder + getUserMedia, the floor - * for any audio-capture engine (whisperWeb + whisperServer). */ - audioCapture: boolean; -}; - -/** - * Detects which voice engines this browser can run. Safe to call during - * SSR — returns all-false instead of throwing on missing globals. - */ -export function detectVoiceCapabilities(): VoiceCapabilities { - if (typeof window === "undefined") { - return { webSpeech: false, whisperWeb: false, audioCapture: false }; - } - const w = window as Window & { - SpeechRecognition?: unknown; - webkitSpeechRecognition?: unknown; - }; - const webSpeech = !!(w.SpeechRecognition || w.webkitSpeechRecognition); - const audioCapture = - typeof navigator !== "undefined" && - typeof navigator.mediaDevices?.getUserMedia === "function" && - typeof window.MediaRecorder !== "undefined"; - // whisper-web piggybacks on transformers.js which only needs a Worker plus - // either WebGPU or WebAssembly. Every modern browser has both, so the - // gating constraint is having MediaRecorder for capture. - const whisperWeb = audioCapture && typeof Worker !== "undefined"; - return { webSpeech, whisperWeb, audioCapture }; -} - -/** - * Resolves the active voice-input engine given a user preference and the - * detected capabilities. Returns null when nothing usable is available. - * - * Auto-fallback order: Web Speech (cheapest, native) → Whisper Web (private, - * heavier) → Whisper Server (always works but requires a configured server). - * If the user pinned a specific engine that isn't available, we degrade - * gracefully along the same order. - */ -export function resolveActiveEngine( - preference: VoiceInputEngine, - caps: VoiceCapabilities, - serverFallbackEnabled: boolean, -): Exclude | null { - const order: Array> = [ - "webSpeech", - "whisperWeb", - "whisperServer", - ]; - - const isUsable = (e: Exclude) => { - if (e === "webSpeech") return caps.webSpeech; - if (e === "whisperWeb") return caps.whisperWeb; - return caps.audioCapture && serverFallbackEnabled; - }; - - if (preference === "auto") { - return order.find(isUsable) ?? null; - } - if (isUsable(preference)) return preference; - // Pinned engine isn't usable — fall through to the next available one in - // the auto order so the button still works instead of silently no-op. - return order.find(isUsable) ?? null; -} diff --git a/apps/web/lib/voice/whisper-web-client.ts b/apps/web/lib/voice/whisper-web-client.ts deleted file mode 100644 index e9d1cc6208..0000000000 --- a/apps/web/lib/voice/whisper-web-client.ts +++ /dev/null @@ -1,199 +0,0 @@ -"use client"; - -import { whisperModelConfig } from "./whisper-web-models"; -import type { WhisperWebModelSize } from "@/lib/types/http-voice"; - -/** - * Sample rate Whisper expects. We resample the captured audio to this rate - * (mono Float32Array) before sending to the worker — Whisper's own decoder - * would do this too, but doing it here keeps the worker focused on inference. - */ -const WHISPER_SAMPLE_RATE = 16000; - -export type WhisperWebProgress = { - stage: string; - progress: number; -}; - -export type WhisperWebHandlers = { - onProgress?: (p: WhisperWebProgress) => void; -}; - -type WorkerMessage = - | { type: "progress"; stage: string; progress: number } - | { type: "ready" } - | { type: "result"; text: string } - | { type: "error"; message: string }; - -type Pending = { - kind: "init" | "transcribe"; - resolve: (value: string | undefined) => void; - reject: (err: Error) => void; -}; - -/** - * Client wrapper around the whisper-web worker. Hides the postMessage - * protocol behind a clean promise-based API and handles the audio decode + - * resample step so callers only see "Blob in, transcript out". - */ -export class WhisperWebClient { - private worker: Worker | null = null; - private pending: Pending | null = null; - private ready = false; - private loadingModelId: string | null = null; - - constructor(private handlers: WhisperWebHandlers = {}) {} - - /** - * Lazy-creates the worker on first use. Returns a promise that resolves - * when the requested model is loaded and ready to transcribe. - */ - async init(size: WhisperWebModelSize): Promise { - const config = whisperModelConfig(size); - if (this.ready && this.loadingModelId === config.modelId) return; - this.ensureWorker(); - this.loadingModelId = config.modelId; - this.ready = false; - await this.send({ kind: "init", payload: { type: "init", model: config.modelId } }); - this.ready = true; - } - - /** - * Transcribe a recorded blob. The blob may be in any container the browser - * can decode (audio/webm, audio/wav, audio/mp4, …) — we resample everything - * to 16 kHz mono Float32 before handing to the worker. - */ - async transcribe(blob: Blob, language?: string): Promise { - if (!this.ready || !this.worker) { - throw new Error("WhisperWebClient: not initialized"); - } - const audio = await blobToWhisperFloat32(blob); - const text = await this.send({ - kind: "transcribe", - payload: { type: "transcribe", audio, language }, - transfer: [audio.buffer], - }); - return text ?? ""; - } - - /** Tear down the worker and release the loaded model. */ - dispose(): void { - if (this.worker) { - try { - this.worker.postMessage({ type: "dispose" }); - } catch { - // ignore - } - this.worker.terminate(); - this.worker = null; - } - this.ready = false; - this.loadingModelId = null; - if (this.pending) { - this.pending.reject(new Error("WhisperWebClient disposed")); - this.pending = null; - } - } - - private ensureWorker() { - if (this.worker) return; - // The `new Worker(new URL(..., import.meta.url))` form is Next.js / webpack's - // recommended pattern — webpack handles the bundling and asset path. - this.worker = new Worker(new URL("../../workers/whisper-web.worker.ts", import.meta.url), { - type: "module", - }); - this.worker.addEventListener("message", (e: MessageEvent) => - this.handleMessage(e.data), - ); - // Capture the worker reference at listener-attach time. A late error from - // a previously-disposed worker can still bubble up after we've already - // created its replacement; without the identity check below, that stale - // event would terminate the brand-new worker too. - const ownWorker = this.worker; - this.worker.addEventListener("error", (e) => { - const err = new Error(e.message || "Whisper worker crashed"); - ownWorker?.terminate(); - // Only clear our refs if this is still the active worker — a stale - // error from a worker we already replaced must not nuke the new one. - if (this.worker === ownWorker) { - this.worker = null; - this.ready = false; - this.loadingModelId = null; - } - if (this.pending) { - this.pending.reject(err); - this.pending = null; - } - }); - } - - private send(args: { - kind: "init" | "transcribe"; - payload: object; - transfer?: Transferable[]; - }): Promise { - if (!this.worker) throw new Error("WhisperWebClient: worker not initialized"); - if (this.pending) { - return Promise.reject(new Error("WhisperWebClient: another request is in flight")); - } - return new Promise((resolve, reject) => { - this.pending = { kind: args.kind, resolve, reject }; - this.worker?.postMessage(args.payload, args.transfer ?? []); - }); - } - - private handleMessage(msg: WorkerMessage) { - if (msg.type === "progress") { - this.handlers.onProgress?.({ stage: msg.stage, progress: msg.progress }); - return; - } - const pending = this.pending; - if (!pending) return; - this.pending = null; - if (msg.type === "error") { - pending.reject(new Error(msg.message)); - return; - } - if (msg.type === "ready") { - pending.resolve(undefined); - return; - } - if (msg.type === "result") { - pending.resolve(msg.text); - } - } -} - -/** - * Decode an arbitrary audio Blob and return a Float32Array sampled at 16 kHz - * mono — the format Whisper expects. - */ -export async function blobToWhisperFloat32(blob: Blob): Promise { - const arrayBuffer = await blob.arrayBuffer(); - // Decode using an AudioContext at the source rate, then bounce through an - // OfflineAudioContext for the resample. AudioContext.decodeAudioData - // tolerates webm/opus, mp4/aac, wav, ogg — anything the browser can play. - const AudioCtor = - window.AudioContext ?? - (window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext; - if (!AudioCtor) throw new Error("AudioContext is not available in this browser"); - const decodeCtx = new AudioCtor(); - let decoded: AudioBuffer; - try { - decoded = await decodeCtx.decodeAudioData(arrayBuffer); - } finally { - await decodeCtx.close(); - } - return resampleToMono16k(decoded); -} - -async function resampleToMono16k(buf: AudioBuffer): Promise { - const length = Math.ceil((buf.duration * WHISPER_SAMPLE_RATE) / 1); - const offline = new OfflineAudioContext(1, length, WHISPER_SAMPLE_RATE); - const source = offline.createBufferSource(); - source.buffer = buf; - source.connect(offline.destination); - source.start(0); - const rendered = await offline.startRendering(); - return rendered.getChannelData(0).slice(); -} diff --git a/apps/web/lib/voice/whisper-web-models.ts b/apps/web/lib/voice/whisper-web-models.ts deleted file mode 100644 index eaffe6698a..0000000000 --- a/apps/web/lib/voice/whisper-web-models.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { WhisperWebModelSize } from "@/lib/types/http-voice"; - -export type WhisperModelConfig = { - size: WhisperWebModelSize; - /** Hugging Face model id. Use the `onnx-community/*` mirrors — `Xenova/*` - * defaults to 4-bit MatMulNBits weights that crash on WASM (see note below). */ - modelId: string; - /** Rough on-disk size after download, shown in the settings UI. */ - approxBytes: number; - /** Human-readable label. */ - label: string; -}; - -// The `onnx-community/whisper-*` mirrors are the maintained transformers.js -// exports. The older `Xenova/whisper-*` mirrors default to 4-bit (`MatMulNBits`) -// weights that only run on WebGPU — on WASM they fail with -// `Missing required scale: ... weight_merged_0_scale`. The onnx-community -// mirrors include the q8 variant we pin to in the worker. -export const WHISPER_WEB_MODELS: Record = { - tiny: { - size: "tiny", - modelId: "onnx-community/whisper-tiny", - approxBytes: 40 * 1024 * 1024, - label: "Whisper Tiny", - }, - base: { - size: "base", - modelId: "onnx-community/whisper-base", - approxBytes: 75 * 1024 * 1024, - label: "Whisper Base", - }, - small: { - size: "small", - modelId: "onnx-community/whisper-small", - approxBytes: 240 * 1024 * 1024, - label: "Whisper Small", - }, -}; - -export function whisperModelConfig(size: WhisperWebModelSize): WhisperModelConfig { - return WHISPER_WEB_MODELS[size] ?? WHISPER_WEB_MODELS.base; -} diff --git a/apps/web/package.json b/apps/web/package.json index 4f513dcd51..b7bffb2234 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -51,7 +51,6 @@ "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", - "@huggingface/transformers": "^4.2.0", "@kandev/theme": "workspace:*", "@kandev/types": "workspace:*", "@kandev/ui": "workspace:*", diff --git a/apps/web/src/locales/en/settings.json b/apps/web/src/locales/en/settings.json index ae40f96d7d..344fe22ac4 100644 --- a/apps/web/src/locales/en/settings.json +++ b/apps/web/src/locales/en/settings.json @@ -203,11 +203,6 @@ "discoveryAliasesTranscriptNavigation": "anchored prompt bar,scroll to prompt,transcript auto scroll", "discoveryAliasesUtilityActions": "title generation,branch naming,commit message,PR description", "discoveryAliasesUtilityDefaultModel": "utility model,inference model,default AI model", - "discoveryAliasesVoiceBehavior": "voice language,push to talk,auto send,voice activation", - "discoveryAliasesVoiceEnable": "voice input,microphone,speech input", - "discoveryAliasesVoiceEngine": "speech recognition,transcription,Whisper Web", - "discoveryAliasesVoiceShortcut": "voice hotkey,microphone shortcut,push to talk shortcut", - "discoveryVoiceShortcut": "Voice input shortcut", "disk": "Disk %", "diskPath": "Disk path", "displayChangedFilesAsAFlat": "Display changed files as a flat list with full paths, or as a folder tree.", @@ -703,56 +698,6 @@ "utilityProfileNeedsRepair": "This profile is unavailable. Select an enabled inference profile.", "viewAvailableSettings": "View available settings", "visualEditorUnavailable": "Visual editor unavailable", - "voiceActivation": "Activation", - "voiceActivationHold": "Hold to talk", - "voiceActivationToggle": "Click to start / stop", - "voiceAutoSend": "Auto-send after transcription", - "voiceAutoSendDescription": "Submit the message as soon as the transcript is inserted.", - "voiceBehavior": "Behavior", - "voiceEnableDescription": "When off, the voice button is hidden entirely and no voice-related code runs. Settings below are preserved and re-applied when you turn it back on.", - "voiceEnableLabel": "Show the mic button on the chat composer", - "voiceEnableTitle": "Enable Voice Input", - "voiceEngineAuto": "Automatic", - "voiceEngineAutoDescription": "Use the best engine available in this browser.", - "voiceEngineBadgeLocal": "Local", - "voiceEngineBadgeServer": "Server", - "voiceEngineTitle": "Transcription Engine", - "voiceEngineUnsupported": "Not supported in this browser.", - "voiceEngineWebSpeech": "Web Speech (in-browser)", - "voiceEngineWebSpeechDescription": "Free, instant, uses your browser's built-in speech recognition.", - "voiceEngineWhisperServer": "Whisper Server (OpenAI)", - "voiceEngineWhisperServerDescription": "Sends audio to the backend, which forwards it to OpenAI's Whisper API. Requires a configured API key on the server.", - "voiceEngineWhisperWeb": "Whisper Web (private, in-browser)", - "voiceEngineWhisperWebDescription": "Runs OpenAI Whisper entirely on this device. First use downloads the model (40–240 MB).", - "voiceLanguageAuto": "Auto-detect (browser language)", - "voiceLanguageDeDe": "German", - "voiceLanguageEnGb": "English (United Kingdom)", - "voiceLanguageEnUs": "English (United States)", - "voiceLanguageEsEs": "Spanish (Spain)", - "voiceLanguageEsMx": "Spanish (Mexico)", - "voiceLanguageFrFr": "French", - "voiceLanguageHint": "Recognition quality drops sharply when the language doesn't match what you're speaking.", - "voiceLanguageItIt": "Italian", - "voiceLanguageJaJp": "Japanese", - "voiceLanguagePtBr": "Portuguese (Brazil)", - "voiceLanguagePtPt": "Portuguese (Portugal)", - "voiceLanguageZhCn": "Chinese (Simplified)", - "voiceLanguagesGroup": "Languages", - "voiceMode": "Voice Mode", - "voiceModePageDescription": "Configure how voice input works on the chat composer.", - "voiceShortcutHint": "Click the shortcut to record a new key combination. All keyboard shortcuts can also be edited in General Settings.", - "voiceShortcutTitle": "{{shortcut}} Shortcut", - "voiceUnavailableInsecure": "Microphone APIs require {{secureScheme}} or {{localhostHost}}. You appear to be on an insecure {{insecureScheme}} origin; load this page over {{secureScheme}} (or {{localhostUrl}}) to enable voice input.", - "voiceUnavailableTitle": "Voice input is unavailable in this browser.", - "voiceUnavailableUnsupported": "Your browser doesn't expose either the Web Speech API or MediaRecorder. Try Chrome, Edge, or Safari 14.5+.", - "voiceWhisperBase": "Base", - "voiceWhisperBaseHint": "Balanced default", - "voiceWhisperModelHint": "The model downloads on first use and is cached in your browser. Switching models triggers another download next time you record.", - "voiceWhisperModelTitle": "Whisper Web Model", - "voiceWhisperSmall": "Small", - "voiceWhisperSmallHint": "Best accuracy, slower load", - "voiceWhisperTiny": "Tiny", - "voiceWhisperTinyHint": "Fastest, lower accuracy", "vsCodeRemoteSsh": "VS Code Remote SSH", "whenEnabledInstallYourProjectS": "When enabled, install your project's dependencies (e.g. <2>{{command}} via repository setup scripts) to avoid missing type errors.", "externalMcpToolListTaskSessions": "List every agent session on a task, newest first, so another tool can address one by ID." diff --git a/apps/web/src/locales/en/task.json b/apps/web/src/locales/en/task.json index 734fcc4cb0..6204d98e02 100644 --- a/apps/web/src/locales/en/task.json +++ b/apps/web/src/locales/en/task.json @@ -371,7 +371,6 @@ "doubleClickToEditOrPress": "Double-click to edit (or press Enter)", "doubleClickToKeepThisTab": "Double-click to keep this tab open", "download": "Download", - "downloadingModelPercent": "Downloading {{modelLabel}}… {{pct}}%", "downloadingPercent": "Downloading {{modelLabel}}, {{pct}} percent", "draftCreated": "Draft {{longName}} created", "dropFilesHere": "Drop files here", @@ -1043,8 +1042,6 @@ "requestedChangesPlain": "{{who}} requested changes", "requestedChangesVerb": "Requested changes", "requestedChangesWithComment": "{{who}} requested changes: '{{comment}}'", - "requestingMicrophone": "Requesting microphone…", - "requestingMicrophonePermission": "Requesting microphone permission", "resetAgentContext": "Reset agent context?", "resetAgentContextClearsConversationHistory": "Reset agent context; clears conversation history, preserves workspace", "resetContext": "Reset Context", @@ -1256,7 +1253,6 @@ "startTaskInPlanMode": "Start task in plan mode", "startTunnel": "Start tunnel", "startTypingYourPlan": "Start typing your plan...", - "startVoiceInput": "Start voice input", "started": "Started", "starting": "Starting...", "startingAgent": "Starting {{agentLabel}}", @@ -1284,9 +1280,7 @@ "stillWorkingSubjectOne": "This task is still working", "stillWorkingWarning": "{{subject}}; an agent is generating or running background work. Proceeding now discards work that is still in progress.", "stop": "Stop", - "stopRecording": "Stop recording", "stopTunnel": "Stop tunnel", - "stopVoiceInput": "Stop voice input", "stoppedWithAnError": "stopped with an error", "stopping": "Stopping…", "subagent": "Subagent", @@ -1402,8 +1396,6 @@ "toolCount_other": "{{count}} tools", "tools": "tools", "toolSearching": "Searching", - "transcribing": "Transcribing…", - "transcribingVoiceInput": "Transcribing voice input", "treeActionFailed": "Tree action failed", "treeCancelDescription": "This will cancel {{taskCount}} tasks and interrupt {{activeRunCount}} active agent sessions. This action can be undone with Restore.", "treeCost": "Tree cost", @@ -1471,13 +1463,6 @@ "viewLimitReached_other": "You can save up to {{count}} views.", "viewName": "View name", "viewPlanHistory": "View plan history", - "voiceInput": "Voice input", - "voiceInputNeedsHttps": "Voice input needs HTTPS. Open this site over {{secureSchemeUrl}} (or {{localhostUrl}}); most mobile browsers block microphone APIs on insecure origins.", - "voiceInputUnavailable": "Voice input unavailable", - "voiceInputUnavailableHere": "Voice input is unavailable here.", - "voiceInputUnavailableTapForDetails": "Voice input unavailable; tap for details", - "voiceInputUnsupportedBrowser": "Voice input isn't supported in this browser. Try Chrome, Edge, or Safari 14.5+.", - "voiceTooltipHold": "{{tooltip}} (hold)", "vsCode": "VS Code", "vsCodeServer": "VS Code Server", "waitForActiveTurnToFinish": "Wait for the active turn or tool call to finish before adding sources.", diff --git a/apps/web/src/locales/pseudo/settings.json b/apps/web/src/locales/pseudo/settings.json index 8693182ac6..5056e76318 100644 --- a/apps/web/src/locales/pseudo/settings.json +++ b/apps/web/src/locales/pseudo/settings.json @@ -203,11 +203,6 @@ "discoveryAliasesTranscriptNavigation": "àńćĥōŕēď ƥŕōḿƥţ ƀàŕ,śćŕōĺĺ ţō ƥŕōḿƥţ,ţŕàńśćŕĩƥţ àũţō śćŕōĺĺ", "discoveryAliasesUtilityActions": "ţĩţĺē ĝēńēŕàţĩōń,ƀŕàńćĥ ńàḿĩńĝ,ćōḿḿĩţ ḿēśśàĝē,ƤŔ ďēśćŕĩƥţĩōń", "discoveryAliasesUtilityDefaultModel": "ũţĩĺĩţŷ ḿōďēĺ,ĩńƒēŕēńćē ḿōďēĺ,ďēƒàũĺţ ÀĨ ḿōďēĺ", - "discoveryAliasesVoiceBehavior": "vōĩćē ĺàńĝũàĝē,ƥũśĥ ţō ţàĺķ,àũţō śēńď,vōĩćē àćţĩvàţĩōń", - "discoveryAliasesVoiceEnable": "vōĩćē ĩńƥũţ,ḿĩćŕōƥĥōńē,śƥēēćĥ ĩńƥũţ", - "discoveryAliasesVoiceEngine": "śƥēēćĥ ŕēćōĝńĩţĩōń,ţŕàńśćŕĩƥţĩōń,Ŵĥĩśƥēŕ Ŵēƀ", - "discoveryAliasesVoiceShortcut": "vōĩćē ĥōţķēŷ,ḿĩćŕōƥĥōńē śĥōŕţćũţ,ƥũśĥ ţō ţàĺķ śĥōŕţćũţ", - "discoveryVoiceShortcut": "Vōĩćē ĩńƥũţ śĥōŕţćũţ", "disk": "Ďĩśķ %", "diskPath": "Ďĩśķ ƥàţĥ", "displayChangedFilesAsAFlat": "Ďĩśƥĺàŷ ćĥàńĝēď ƒĩĺēś àś à ƒĺàţ ĺĩśţ ŵĩţĥ ƒũĺĺ ƥàţĥś, ōŕ àś à ƒōĺďēŕ ţŕēē.", @@ -703,56 +698,6 @@ "utilityProfileNeedsRepair": "Ţĥĩś ƥŕōƒĩĺē ĩś ũńàvàĩĺàƀĺē. Śēĺēćţ àń ēńàƀĺēď ĩńƒēŕēńćē ƥŕōƒĩĺē.", "viewAvailableSettings": "Vĩēŵ àvàĩĺàƀĺē śēţţĩńĝś", "visualEditorUnavailable": "Vĩśũàĺ ēďĩţōŕ ũńàvàĩĺàƀĺē", - "voiceActivation": "Àćţĩvàţĩōń", - "voiceActivationHold": "Ĥōĺď ţō ţàĺķ", - "voiceActivationToggle": "Ćĺĩćķ ţō śţàŕţ / śţōƥ", - "voiceAutoSend": "Àũţō-śēńď àƒţēŕ ţŕàńśćŕĩƥţĩōń", - "voiceAutoSendDescription": "Śũƀḿĩţ ţĥē ḿēśśàĝē àś śōōń àś ţĥē ţŕàńśćŕĩƥţ ĩś ĩńśēŕţēď.", - "voiceBehavior": "Ɓēĥàvĩōŕ", - "voiceEnableDescription": "Ŵĥēń ōƒƒ, ţĥē vōĩćē ƀũţţōń ĩś ĥĩďďēń ēńţĩŕēĺŷ àńď ńō vōĩćē-ŕēĺàţēď ćōďē ŕũńś. Śēţţĩńĝś ƀēĺōŵ àŕē ƥŕēśēŕvēď àńď ŕē-àƥƥĺĩēď ŵĥēń ŷōũ ţũŕń ĩţ ƀàćķ ōń.", - "voiceEnableLabel": "Śĥōŵ ţĥē ḿĩć ƀũţţōń ōń ţĥē ćĥàţ ćōḿƥōśēŕ", - "voiceEnableTitle": "Ēńàƀĺē Vōĩćē Ĩńƥũţ", - "voiceEngineAuto": "Àũţōḿàţĩć", - "voiceEngineAutoDescription": "Ũśē ţĥē ƀēśţ ēńĝĩńē àvàĩĺàƀĺē ĩń ţĥĩś ƀŕōŵśēŕ.", - "voiceEngineBadgeLocal": "Ĺōćàĺ", - "voiceEngineBadgeServer": "Śēŕvēŕ", - "voiceEngineTitle": "Ţŕàńśćŕĩƥţĩōń Ēńĝĩńē", - "voiceEngineUnsupported": "Ńōţ śũƥƥōŕţēď ĩń ţĥĩś ƀŕōŵśēŕ.", - "voiceEngineWebSpeech": "Ŵēƀ Śƥēēćĥ (ĩń-ƀŕōŵśēŕ)", - "voiceEngineWebSpeechDescription": "Ƒŕēē, ĩńśţàńţ, ũśēś ŷōũŕ ƀŕōŵśēŕ'ś ƀũĩĺţ-ĩń śƥēēćĥ ŕēćōĝńĩţĩōń.", - "voiceEngineWhisperServer": "Ŵĥĩśƥēŕ Śēŕvēŕ (ŌƥēńÀĨ)", - "voiceEngineWhisperServerDescription": "Śēńďś àũďĩō ţō ţĥē ƀàćķēńď, ŵĥĩćĥ ƒōŕŵàŕďś ĩţ ţō ŌƥēńÀĨ'ś Ŵĥĩśƥēŕ ÀƤĨ. Ŕēqũĩŕēś à ćōńƒĩĝũŕēď ÀƤĨ ķēŷ ōń ţĥē śēŕvēŕ.", - "voiceEngineWhisperWeb": "Ŵĥĩśƥēŕ Ŵēƀ (ƥŕĩvàţē, ĩń-ƀŕōŵśēŕ)", - "voiceEngineWhisperWebDescription": "Ŕũńś ŌƥēńÀĨ Ŵĥĩśƥēŕ ēńţĩŕēĺŷ ōń ţĥĩś ďēvĩćē. Ƒĩŕśţ ũśē ďōŵńĺōàďś ţĥē ḿōďēĺ (40–240 ḾƁ).", - "voiceLanguageAuto": "Àũţō-ďēţēćţ (ƀŕōŵśēŕ ĺàńĝũàĝē)", - "voiceLanguageDeDe": "Ĝēŕḿàń", - "voiceLanguageEnGb": "Ēńĝĺĩśĥ (Ũńĩţēď Ķĩńĝďōḿ)", - "voiceLanguageEnUs": "Ēńĝĺĩśĥ (Ũńĩţēď Śţàţēś)", - "voiceLanguageEsEs": "Śƥàńĩśĥ (Śƥàĩń)", - "voiceLanguageEsMx": "Śƥàńĩśĥ (Ḿēxĩćō)", - "voiceLanguageFrFr": "Ƒŕēńćĥ", - "voiceLanguageHint": "Ŕēćōĝńĩţĩōń qũàĺĩţŷ ďŕōƥś śĥàŕƥĺŷ ŵĥēń ţĥē ĺàńĝũàĝē ďōēśń'ţ ḿàţćĥ ŵĥàţ ŷōũ'ŕē śƥēàķĩńĝ.", - "voiceLanguageItIt": "Ĩţàĺĩàń", - "voiceLanguageJaJp": "Ĵàƥàńēśē", - "voiceLanguagePtBr": "Ƥōŕţũĝũēśē (Ɓŕàźĩĺ)", - "voiceLanguagePtPt": "Ƥōŕţũĝũēśē (Ƥōŕţũĝàĺ)", - "voiceLanguageZhCn": "Ćĥĩńēśē (Śĩḿƥĺĩƒĩēď)", - "voiceLanguagesGroup": "Ĺàńĝũàĝēś", - "voiceMode": "Vōĩćē Ḿōďē", - "voiceModePageDescription": "Ćōńƒĩĝũŕē ĥōŵ vōĩćē ĩńƥũţ ŵōŕķś ōń ţĥē ćĥàţ ćōḿƥōśēŕ.", - "voiceShortcutHint": "Ćĺĩćķ ţĥē śĥōŕţćũţ ţō ŕēćōŕď à ńēŵ ķēŷ ćōḿƀĩńàţĩōń. Àĺĺ ķēŷƀōàŕď śĥōŕţćũţś ćàń àĺśō ƀē ēďĩţēď ĩń Ĝēńēŕàĺ Śēţţĩńĝś.", - "voiceShortcutTitle": "{{shortcut}} Śĥōŕţćũţ", - "voiceUnavailableInsecure": "Ḿĩćŕōƥĥōńē ÀƤĨś ŕēqũĩŕē {{secureScheme}} ōŕ {{localhostHost}}. Ŷōũ àƥƥēàŕ ţō ƀē ōń àń ĩńśēćũŕē {{insecureScheme}} ōŕĩĝĩń; ĺōàď ţĥĩś ƥàĝē ōvēŕ {{secureScheme}} (ōŕ {{localhostUrl}}) ţō ēńàƀĺē vōĩćē ĩńƥũţ.", - "voiceUnavailableTitle": "Vōĩćē ĩńƥũţ ĩś ũńàvàĩĺàƀĺē ĩń ţĥĩś ƀŕōŵśēŕ.", - "voiceUnavailableUnsupported": "Ŷōũŕ ƀŕōŵśēŕ ďōēśń'ţ ēxƥōśē ēĩţĥēŕ ţĥē Ŵēƀ Śƥēēćĥ ÀƤĨ ōŕ ḾēďĩàŔēćōŕďēŕ. Ţŕŷ Ćĥŕōḿē, Ēďĝē, ōŕ Śàƒàŕĩ 14.5+.", - "voiceWhisperBase": "Ɓàśē", - "voiceWhisperBaseHint": "Ɓàĺàńćēď ďēƒàũĺţ", - "voiceWhisperModelHint": "Ţĥē ḿōďēĺ ďōŵńĺōàďś ōń ƒĩŕśţ ũśē àńď ĩś ćàćĥēď ĩń ŷōũŕ ƀŕōŵśēŕ. Śŵĩţćĥĩńĝ ḿōďēĺś ţŕĩĝĝēŕś àńōţĥēŕ ďōŵńĺōàď ńēxţ ţĩḿē ŷōũ ŕēćōŕď.", - "voiceWhisperModelTitle": "Ŵĥĩśƥēŕ Ŵēƀ Ḿōďēĺ", - "voiceWhisperSmall": "Śḿàĺĺ", - "voiceWhisperSmallHint": "Ɓēśţ àććũŕàćŷ, śĺōŵēŕ ĺōàď", - "voiceWhisperTiny": "Ţĩńŷ", - "voiceWhisperTinyHint": "Ƒàśţēśţ, ĺōŵēŕ àććũŕàćŷ", "vsCodeRemoteSsh": "VŚ Ćōďē Ŕēḿōţē ŚŚĤ", "whenEnabledInstallYourProjectS": "Ŵĥēń ēńàƀĺēď, ĩńśţàĺĺ ŷōũŕ ƥŕōĵēćţ'ś ďēƥēńďēńćĩēś (ē.ĝ. <2>{{command}} vĩà ŕēƥōśĩţōŕŷ śēţũƥ śćŕĩƥţś) ţō àvōĩď ḿĩśśĩńĝ ţŷƥē ēŕŕōŕś.", "externalMcpToolListTaskSessions": "Ĺĩśţ ēvēŕŷ àĝēńţ śēśśĩōń ōń à ţàśķ, ńēŵēśţ ƒĩŕśţ, śō àńōţĥēŕ ţōōĺ ćàń àďďŕēśś ōńē ƀŷ ĨĎ." diff --git a/apps/web/src/locales/pseudo/task.json b/apps/web/src/locales/pseudo/task.json index f2f0c2d560..f8021a5475 100644 --- a/apps/web/src/locales/pseudo/task.json +++ b/apps/web/src/locales/pseudo/task.json @@ -371,7 +371,6 @@ "doubleClickToEditOrPress": "Ďōũƀĺē-ćĺĩćķ ţō ēďĩţ (ōŕ ƥŕēśś Ēńţēŕ)", "doubleClickToKeepThisTab": "Ďōũƀĺē-ćĺĩćķ ţō ķēēƥ ţĥĩś ţàƀ ōƥēń", "download": "Ďōŵńĺōàď", - "downloadingModelPercent": "Ďōŵńĺōàďĩńĝ {{modelLabel}}… {{pct}}%", "downloadingPercent": "Ďōŵńĺōàďĩńĝ {{modelLabel}}, {{pct}} ƥēŕćēńţ", "draftCreated": "Ďŕàƒţ {{longName}} ćŕēàţēď", "dropFilesHere": "Ďŕōƥ ƒĩĺēś ĥēŕē", @@ -1043,8 +1042,6 @@ "requestedChangesPlain": "{{who}} ŕēqũēśţēď ćĥàńĝēś", "requestedChangesVerb": "Ŕēqũēśţēď ćĥàńĝēś", "requestedChangesWithComment": "{{who}} ŕēqũēśţēď ćĥàńĝēś: '{{comment}}'", - "requestingMicrophone": "Ŕēqũēśţĩńĝ ḿĩćŕōƥĥōńē…", - "requestingMicrophonePermission": "Ŕēqũēśţĩńĝ ḿĩćŕōƥĥōńē ƥēŕḿĩśśĩōń", "resetAgentContext": "Ŕēśēţ àĝēńţ ćōńţēxţ?", "resetAgentContextClearsConversationHistory": "Ŕēśēţ àĝēńţ ćōńţēxţ; ćĺēàŕś ćōńvēŕśàţĩōń ĥĩśţōŕŷ, ƥŕēśēŕvēś ŵōŕķśƥàćē", "resetContext": "Ŕēśēţ Ćōńţēxţ", @@ -1256,7 +1253,6 @@ "startTaskInPlanMode": "Śţàŕţ ţàśķ ĩń ƥĺàń ḿōďē", "startTunnel": "Śţàŕţ ţũńńēĺ", "startTypingYourPlan": "Śţàŕţ ţŷƥĩńĝ ŷōũŕ ƥĺàń...", - "startVoiceInput": "Śţàŕţ vōĩćē ĩńƥũţ", "started": "Śţàŕţēď", "starting": "Śţàŕţĩńĝ...", "startingAgent": "Śţàŕţĩńĝ {{agentLabel}}", @@ -1284,9 +1280,7 @@ "stillWorkingSubjectOne": "Ţĥĩś ţàśķ ĩś śţĩĺĺ ŵōŕķĩńĝ", "stillWorkingWarning": "{{subject}}; àń àĝēńţ ĩś ĝēńēŕàţĩńĝ ōŕ ŕũńńĩńĝ ƀàćķĝŕōũńď ŵōŕķ. Ƥŕōćēēďĩńĝ ńōŵ ďĩśćàŕďś ŵōŕķ ţĥàţ ĩś śţĩĺĺ ĩń ƥŕōĝŕēśś.", "stop": "Śţōƥ", - "stopRecording": "Śţōƥ ŕēćōŕďĩńĝ", "stopTunnel": "Śţōƥ ţũńńēĺ", - "stopVoiceInput": "Śţōƥ vōĩćē ĩńƥũţ", "stoppedWithAnError": "śţōƥƥēď ŵĩţĥ àń ēŕŕōŕ", "stopping": "Śţōƥƥĩńĝ…", "subagent": "Śũƀàĝēńţ", @@ -1402,8 +1396,6 @@ "toolCount_other": "{{count}} ţōōĺś", "tools": "ţōōĺś", "toolSearching": "Śēàŕćĥĩńĝ", - "transcribing": "Ţŕàńśćŕĩƀĩńĝ…", - "transcribingVoiceInput": "Ţŕàńśćŕĩƀĩńĝ vōĩćē ĩńƥũţ", "treeActionFailed": "Ţŕēē àćţĩōń ƒàĩĺēď", "treeCancelDescription": "Ţĥĩś ŵĩĺĺ ćàńćēĺ {{taskCount}} ţàśķś àńď ĩńţēŕŕũƥţ {{activeRunCount}} àćţĩvē àĝēńţ śēśśĩōńś. Ţĥĩś àćţĩōń ćàń ƀē ũńďōńē ŵĩţĥ Ŕēśţōŕē.", "treeCost": "Ţŕēē ćōśţ", @@ -1471,13 +1463,6 @@ "viewLimitReached_other": "Ŷōũ ćàń śàvē ũƥ ţō {{count}} vĩēŵś.", "viewName": "Vĩēŵ ńàḿē", "viewPlanHistory": "Vĩēŵ ƥĺàń ĥĩśţōŕŷ", - "voiceInput": "Vōĩćē ĩńƥũţ", - "voiceInputNeedsHttps": "Vōĩćē ĩńƥũţ ńēēďś ĤŢŢƤŚ. Ōƥēń ţĥĩś śĩţē ōvēŕ {{secureSchemeUrl}} (ōŕ {{localhostUrl}}); ḿōśţ ḿōƀĩĺē ƀŕōŵśēŕś ƀĺōćķ ḿĩćŕōƥĥōńē ÀƤĨś ōń ĩńśēćũŕē ōŕĩĝĩńś.", - "voiceInputUnavailable": "Vōĩćē ĩńƥũţ ũńàvàĩĺàƀĺē", - "voiceInputUnavailableHere": "Vōĩćē ĩńƥũţ ĩś ũńàvàĩĺàƀĺē ĥēŕē.", - "voiceInputUnavailableTapForDetails": "Vōĩćē ĩńƥũţ ũńàvàĩĺàƀĺē; ţàƥ ƒōŕ ďēţàĩĺś", - "voiceInputUnsupportedBrowser": "Vōĩćē ĩńƥũţ ĩśń'ţ śũƥƥōŕţēď ĩń ţĥĩś ƀŕōŵśēŕ. Ţŕŷ Ćĥŕōḿē, Ēďĝē, ōŕ Śàƒàŕĩ 14.5+.", - "voiceTooltipHold": "{{tooltip}} (ĥōĺď)", "vsCode": "VŚ Ćōďē", "vsCodeServer": "VŚ Ćōďē Śēŕvēŕ", "waitForActiveTurnToFinish": "Ŵàĩţ ƒōŕ ţĥē àćţĩvē ţũŕń ōŕ ţōōĺ ćàĺĺ ţō ƒĩńĩśĥ ƀēƒōŕē àďďĩńĝ śōũŕćēś.", diff --git a/apps/web/src/locales/pt-pt/settings.json b/apps/web/src/locales/pt-pt/settings.json index a83220e8a5..efda6f9eaf 100644 --- a/apps/web/src/locales/pt-pt/settings.json +++ b/apps/web/src/locales/pt-pt/settings.json @@ -200,11 +200,6 @@ "discoveryAliasesTranscriptNavigation": "barra de prompt fixa,ir para o prompt,deslocamento automático da transcrição", "discoveryAliasesUtilityActions": "geração de títulos,nomes de branch,mensagem de commit,descrição de PR", "discoveryAliasesUtilityDefaultModel": "modelo utilitário,modelo de inferência,modelo de IA pré-definido", - "discoveryAliasesVoiceBehavior": "idioma da voz,premir para falar,envio automático,ativação por voz", - "discoveryAliasesVoiceEnable": "entrada de voz,microfone,entrada por fala", - "discoveryAliasesVoiceEngine": "reconhecimento de fala,transcrição,Whisper Web", - "discoveryAliasesVoiceShortcut": "atalho de voz,atalho do microfone,atalho premir para falar", - "discoveryVoiceShortcut": "Atalho de entrada de voz", "disk": "Disco %", "diskPath": "Caminho do disco", "displayChangedFilesAsAFlat": "Apresentar os ficheiros alterados como uma lista simples com caminhos completos ou como uma árvore de pastas.", @@ -688,56 +683,6 @@ "utilityProfileNeedsRepair": "Este perfil está indisponível. Selecione um perfil de inferência ativo.", "viewAvailableSettings": "Ver as definições disponíveis", "visualEditorUnavailable": "Editor visual indisponível", - "voiceActivation": "Ativação", - "voiceActivationHold": "Manter premido para falar", - "voiceActivationToggle": "Clicar para iniciar / parar", - "voiceAutoSend": "Enviar automaticamente após a transcrição", - "voiceAutoSendDescription": "Submeter a mensagem assim que a transcrição for inserida.", - "voiceBehavior": "Comportamento", - "voiceEnableDescription": "Quando está desligado, o botão de voz fica totalmente oculto e não corre nenhum código relacionado com voz. As definições abaixo são preservadas e voltam a ser aplicadas quando o reativar.", - "voiceEnableLabel": "Mostrar o botão de microfone no compositor de chat", - "voiceEnableTitle": "Ativar a entrada de voz", - "voiceEngineAuto": "Automático", - "voiceEngineAutoDescription": "Usar o melhor motor disponível neste navegador.", - "voiceEngineBadgeLocal": "Local", - "voiceEngineBadgeServer": "Servidor", - "voiceEngineTitle": "Motor de transcrição", - "voiceEngineUnsupported": "Não suportado neste navegador.", - "voiceEngineWebSpeech": "Web Speech (no navegador)", - "voiceEngineWebSpeechDescription": "Gratuito, instantâneo, usa o reconhecimento de fala incorporado do seu navegador.", - "voiceEngineWhisperServer": "Whisper Server (OpenAI)", - "voiceEngineWhisperServerDescription": "Envia o áudio para o backend, que o reencaminha para a API Whisper da OpenAI. Requer uma chave de API configurada no servidor.", - "voiceEngineWhisperWeb": "Whisper Web (privado, no navegador)", - "voiceEngineWhisperWebDescription": "Executa o OpenAI Whisper inteiramente neste dispositivo. A primeira utilização transfere o modelo (40–240 MB).", - "voiceLanguageAuto": "Detetar automaticamente (idioma do navegador)", - "voiceLanguageDeDe": "Alemão", - "voiceLanguageEnGb": "Inglês (Reino Unido)", - "voiceLanguageEnUs": "Inglês (Estados Unidos)", - "voiceLanguageEsEs": "Espanhol (Espanha)", - "voiceLanguageEsMx": "Espanhol (México)", - "voiceLanguageFrFr": "Francês", - "voiceLanguageHint": "A qualidade do reconhecimento baixa muito quando o idioma não corresponde ao que está a falar.", - "voiceLanguageItIt": "Italiano", - "voiceLanguageJaJp": "Japonês", - "voiceLanguagePtBr": "Português (Brasil)", - "voiceLanguagePtPt": "Português (Portugal)", - "voiceLanguageZhCn": "Chinês (simplificado)", - "voiceLanguagesGroup": "Idiomas", - "voiceMode": "Modo de voz", - "voiceModePageDescription": "Configure como funciona a entrada de voz no compositor de chat.", - "voiceShortcutHint": "Clique no atalho para gravar uma nova combinação de teclas. Todos os atalhos de teclado também podem ser editados nas Definições gerais.", - "voiceShortcutTitle": "Atalho de {{shortcut}}", - "voiceUnavailableInsecure": "As APIs de microfone exigem {{secureScheme}} ou {{localhostHost}}. Parece estar numa origem {{insecureScheme}} insegura; carregue esta página em {{secureScheme}} (ou {{localhostUrl}}) para ativar a entrada de voz.", - "voiceUnavailableTitle": "A entrada de voz não está disponível neste navegador.", - "voiceUnavailableUnsupported": "O seu navegador não expõe a Web Speech API nem o MediaRecorder. Experimente o Chrome, o Edge ou o Safari 14.5+.", - "voiceWhisperBase": "Base", - "voiceWhisperBaseHint": "Pré-definição equilibrada", - "voiceWhisperModelHint": "O modelo é transferido na primeira utilização e fica em cache no seu navegador. Mudar de modelo desencadeia outra transferência da próxima vez que gravar.", - "voiceWhisperModelTitle": "Modelo do Whisper Web", - "voiceWhisperSmall": "Small", - "voiceWhisperSmallHint": "Melhor precisão, carregamento mais lento", - "voiceWhisperTiny": "Tiny", - "voiceWhisperTinyHint": "Mais rápido, menor precisão", "vsCodeRemoteSsh": "VS Code Remote SSH", "whenEnabledInstallYourProjectS": "Quando está ativado, instale as dependências do seu projeto (por exemplo, <2>{{command}} através dos scripts de configuração do repositório) para evitar erros de tipos por falta de dependências." } diff --git a/apps/web/src/locales/pt-pt/task.json b/apps/web/src/locales/pt-pt/task.json index d4d76a88ea..ddee8438de 100644 --- a/apps/web/src/locales/pt-pt/task.json +++ b/apps/web/src/locales/pt-pt/task.json @@ -371,7 +371,6 @@ "doubleClickToEditOrPress": "Clique duas vezes para editar (ou prima Enter)", "doubleClickToKeepThisTab": "Clique duas vezes para manter este separador aberto", "download": "Transferir", - "downloadingModelPercent": "A transferir {{modelLabel}}… {{pct}}%", "downloadingPercent": "A transferir {{modelLabel}}, {{pct}} por cento", "draftCreated": "Rascunho de {{longName}} criado", "dropFilesHere": "Largue aqui os ficheiros", @@ -1043,8 +1042,6 @@ "requestedChangesPlain": "{{who}} solicitou alterações", "requestedChangesVerb": "Solicitou alterações", "requestedChangesWithComment": "{{who}} solicitou alterações: '{{comment}}'", - "requestingMicrophone": "A pedir acesso ao microfone…", - "requestingMicrophonePermission": "A pedir permissão de microfone", "resetAgentContext": "Repor o contexto do agente?", "resetAgentContextClearsConversationHistory": "Repor o contexto do agente; limpa o histórico da conversa, preserva a área de trabalho", "resetContext": "Repor o contexto", @@ -1256,7 +1253,6 @@ "startTaskInPlanMode": "Iniciar a tarefa em modo de planeamento", "startTunnel": "Iniciar o túnel", "startTypingYourPlan": "Comece a escrever o seu plano...", - "startVoiceInput": "Iniciar a entrada de voz", "started": "Iniciada", "starting": "A iniciar...", "startingAgent": "A iniciar {{agentLabel}}", @@ -1284,9 +1280,7 @@ "stillWorkingSubjectOne": "Esta tarefa continua a trabalhar", "stillWorkingWarning": "{{subject}}; há um agente a gerar ou a executar trabalho em segundo plano. Avançar agora descarta trabalho que ainda está em curso.", "stop": "Parar", - "stopRecording": "Parar a gravação", "stopTunnel": "Parar o túnel", - "stopVoiceInput": "Parar a entrada de voz", "stoppedWithAnError": "parou com um erro", "stopping": "A parar…", "subagent": "Subagente", @@ -1402,8 +1396,6 @@ "toolCount_other": "{{count}} ferramentas", "tools": "ferramentas", "toolSearching": "A pesquisar", - "transcribing": "A transcrever…", - "transcribingVoiceInput": "A transcrever a entrada de voz", "treeActionFailed": "A ação sobre a árvore falhou", "treeCancelDescription": "Isto cancela {{taskCount}} tarefas e interrompe {{activeRunCount}} sessões de agente ativas. Esta ação pode ser anulada com Restaurar.", "treeCost": "Custo da árvore", @@ -1471,13 +1463,6 @@ "viewLimitReached_other": "Pode guardar até {{count}} vistas.", "viewName": "Nome da vista", "viewPlanHistory": "Ver o histórico do plano", - "voiceInput": "Entrada de voz", - "voiceInputNeedsHttps": "A entrada de voz precisa de HTTPS. Abra este site em {{secureSchemeUrl}} (ou {{localhostUrl}}); a maioria dos navegadores móveis bloqueia as APIs de microfone em origens inseguras.", - "voiceInputUnavailable": "Entrada de voz indisponível", - "voiceInputUnavailableHere": "A entrada de voz não está disponível aqui.", - "voiceInputUnavailableTapForDetails": "Entrada de voz indisponível; toque para ver os detalhes", - "voiceInputUnsupportedBrowser": "A entrada de voz não é suportada neste navegador. Experimente o Chrome, o Edge ou o Safari 14.5+.", - "voiceTooltipHold": "{{tooltip}} (manter premido)", "vsCode": "VS Code", "vsCodeServer": "VS Code Server", "waitForActiveTurnToFinish": "Aguarde que o turno ativo ou a chamada de ferramenta terminem antes de adicionar fontes.", diff --git a/apps/web/src/locales/zh-cn/settings.json b/apps/web/src/locales/zh-cn/settings.json index 42de6395f1..5fc5256432 100644 --- a/apps/web/src/locales/zh-cn/settings.json +++ b/apps/web/src/locales/zh-cn/settings.json @@ -203,11 +203,6 @@ "discoveryAliasesTranscriptNavigation": "锚定提示栏,滚动到提示,记录自动滚动", "discoveryAliasesUtilityActions": "标题生成,分支命名,提交信息,PR 描述", "discoveryAliasesUtilityDefaultModel": "实用模型,推理模型,默认 AI 模型", - "discoveryAliasesVoiceBehavior": "语音语言,按住说话,自动发送,语音激活", - "discoveryAliasesVoiceEnable": "语音输入,麦克风,语音输入", - "discoveryAliasesVoiceEngine": "语音识别,转录,Whisper Web", - "discoveryAliasesVoiceShortcut": "语音快捷键,麦克风快捷键,按住说话快捷键", - "discoveryVoiceShortcut": "语音输入快捷键", "disk": "磁盘 %", "diskPath": "磁盘路径", "displayChangedFilesAsAFlat": "将变更文件显示为包含完整路径的平铺列表,或显示为文件夹树。", @@ -703,56 +698,6 @@ "utilityProfileNeedsRepair": "此配置不可用。请选择启用的推理配置。", "viewAvailableSettings": "查看可用设置", "visualEditorUnavailable": "可视化编辑器不可用", - "voiceActivation": "激活方式", - "voiceActivationHold": "按住说话", - "voiceActivationToggle": "点击开始 / 停止", - "voiceAutoSend": "转写后自动发送", - "voiceAutoSendDescription": "转写插入后立即提交消息。", - "voiceBehavior": "行为", - "voiceEnableDescription": "关闭后,语音按钮完全隐藏,且不运行任何语音相关代码。下方设置会保留,重新开启时再次生效。", - "voiceEnableLabel": "在聊天输入框显示麦克风按钮", - "voiceEnableTitle": "启用语音输入", - "voiceEngineAuto": "自动", - "voiceEngineAutoDescription": "使用此浏览器中可用的最佳引擎。", - "voiceEngineBadgeLocal": "本地", - "voiceEngineBadgeServer": "服务器", - "voiceEngineTitle": "转写引擎", - "voiceEngineUnsupported": "此浏览器不支持。", - "voiceEngineWebSpeech": "Web Speech(浏览器内)", - "voiceEngineWebSpeechDescription": "免费、即时,使用浏览器内置语音识别。", - "voiceEngineWhisperServer": "Whisper Server(OpenAI)", - "voiceEngineWhisperServerDescription": "将音频发送到后端,再转发到 OpenAI 的 Whisper API。需要在服务器上配置 API 密钥。", - "voiceEngineWhisperWeb": "Whisper Web(私有,浏览器内)", - "voiceEngineWhisperWebDescription": "完全在此设备上运行 OpenAI Whisper。首次使用会下载模型(40–240 MB)。", - "voiceLanguageAuto": "自动检测(浏览器语言)", - "voiceLanguageDeDe": "德语", - "voiceLanguageEnGb": "英语(英国)", - "voiceLanguageEnUs": "英语(美国)", - "voiceLanguageEsEs": "西班牙语(西班牙)", - "voiceLanguageEsMx": "西班牙语(墨西哥)", - "voiceLanguageFrFr": "法语", - "voiceLanguageHint": "语言与实际说话不匹配时,识别质量会急剧下降。", - "voiceLanguageItIt": "意大利语", - "voiceLanguageJaJp": "日语", - "voiceLanguagePtBr": "葡萄牙语(巴西)", - "voiceLanguagePtPt": "葡萄牙语(葡萄牙)", - "voiceLanguageZhCn": "中文(简体)", - "voiceLanguagesGroup": "语言", - "voiceMode": "语音模式", - "voiceModePageDescription": "配置聊天输入框中语音输入的工作方式。", - "voiceShortcutHint": "点击快捷键可录制新的按键组合。所有键盘快捷键也可在通用设置中编辑。", - "voiceShortcutTitle": "{{shortcut}} 快捷键", - "voiceUnavailableInsecure": "麦克风 API 需要 {{secureScheme}} 或 {{localhostHost}}。你似乎在不安全的 {{insecureScheme}} 源上; 请通过 {{secureScheme}}(或 {{localhostUrl}})加载此页面以启用语音输入。", - "voiceUnavailableTitle": "此浏览器不支持语音输入。", - "voiceUnavailableUnsupported": "你的浏览器未暴露 Web Speech API 或 MediaRecorder。请尝试 Chrome、Edge 或 Safari 14.5+。", - "voiceWhisperBase": "Base", - "voiceWhisperBaseHint": "均衡默认", - "voiceWhisperModelHint": "模型在首次使用时下载并缓存在浏览器中。切换模型会在下次录音时触发另一次下载。", - "voiceWhisperModelTitle": "Whisper Web 模型", - "voiceWhisperSmall": "Small", - "voiceWhisperSmallHint": "准确度最高,加载较慢", - "voiceWhisperTiny": "Tiny", - "voiceWhisperTinyHint": "最快,准确度较低", "vsCodeRemoteSsh": "VS Code Remote SSH", "whenEnabledInstallYourProjectS": "启用后,会通过仓库设置脚本安装项目依赖(例如运行 <2>{{command}}),以避免缺少类型错误。", "externalMcpToolListTaskSessions": "按最近启动的顺序列出任务的所有代理会话,以便其他工具通过 ID 定位会话。" diff --git a/apps/web/src/locales/zh-cn/task.json b/apps/web/src/locales/zh-cn/task.json index f0877238a1..e7f1107065 100644 --- a/apps/web/src/locales/zh-cn/task.json +++ b/apps/web/src/locales/zh-cn/task.json @@ -371,7 +371,6 @@ "doubleClickToEditOrPress": "双击编辑(或按 Enter)", "doubleClickToKeepThisTab": "双击以保持此标签页打开", "download": "下载", - "downloadingModelPercent": "正在下载 {{modelLabel}}… {{pct}}%", "downloadingPercent": "正在下载 {{modelLabel}},{{pct}} 百分比", "draftCreated": "草稿 {{longName}} 已创建", "dropFilesHere": "将文件拖放到此处", @@ -1043,8 +1042,6 @@ "requestedChangesPlain": "{{who}} 请求了更改", "requestedChangesVerb": "已请求更改", "requestedChangesWithComment": "{{who}} 请求了更改:'{{comment}}'", - "requestingMicrophone": "正在请求麦克风…", - "requestingMicrophonePermission": "正在请求麦克风权限", "resetAgentContext": "重置代理上下文?", "resetAgentContextClearsConversationHistory": "重置代理上下文; 清除对话历史,保留工作区", "resetContext": "重置上下文", @@ -1256,7 +1253,6 @@ "startTaskInPlanMode": "以计划模式启动任务", "startTunnel": "启动隧道", "startTypingYourPlan": "开始输入你的计划…", - "startVoiceInput": "开始语音输入", "started": "已启动", "starting": "正在启动…", "startingAgent": "正在启动 {{agentLabel}}", @@ -1284,9 +1280,7 @@ "stillWorkingSubjectOne": "此任务仍在工作", "stillWorkingWarning": "{{subject}}; 代理正在生成或运行后台工作。现在继续将丢弃仍在进行的工作。", "stop": "停止", - "stopRecording": "停止录制", "stopTunnel": "停止隧道", - "stopVoiceInput": "停止语音输入", "stoppedWithAnError": "因错误而停止", "stopping": "正在停止…", "subagent": "子代理", @@ -1402,8 +1396,6 @@ "toolCount_other": "{{count}} 个工具", "tools": "工具", "toolSearching": "正在搜索", - "transcribing": "正在转写…", - "transcribingVoiceInput": "正在转写语音输入", "treeActionFailed": "任务树操作失败", "treeCancelDescription": "将取消 {{taskCount}} 个任务并中断 {{activeRunCount}} 个活动代理会话。可用“恢复”撤销此操作。", "treeCost": "任务树成本", @@ -1471,13 +1463,6 @@ "viewLimitReached_other": "最多可保存 {{count}} 个视图。", "viewName": "视图名称", "viewPlanHistory": "查看计划历史", - "voiceInput": "语音输入", - "voiceInputNeedsHttps": "语音输入需要 HTTPS。请通过 {{secureSchemeUrl}}(或 {{localhostUrl}})打开此站点; 大多数移动浏览器会在不安全来源上阻止麦克风 API。", - "voiceInputUnavailable": "语音输入不可用", - "voiceInputUnavailableHere": "此处无法使用语音输入。", - "voiceInputUnavailableTapForDetails": "语音输入不可用; 点按查看详情", - "voiceInputUnsupportedBrowser": "此浏览器不支持语音输入。请尝试 Chrome、Edge 或 Safari 14.5+。", - "voiceTooltipHold": "{{tooltip}}(按住)", "vsCode": "VS Code", "vsCodeServer": "VS Code Server", "waitForActiveTurnToFinish": "请等待当前回合或工具调用完成后再添加来源。", diff --git a/apps/web/workers/whisper-web.worker.ts b/apps/web/workers/whisper-web.worker.ts deleted file mode 100644 index 68fa33e4b5..0000000000 --- a/apps/web/workers/whisper-web.worker.ts +++ /dev/null @@ -1,138 +0,0 @@ -/// - -/** - * Web Worker that runs OpenAI Whisper entirely in the browser via - * @huggingface/transformers (the maintained transformers.js library that - * xenova/whisper-web is built on). - * - * Lives in its own worker because model loading + inference both block the - * main thread for several seconds — would freeze the chat input otherwise. - * - * Wire protocol (postMessage): - * in: { type: "init", model: "onnx-community/whisper-base" } - * in: { type: "transcribe", audio: Float32Array, language?: string } - * in: { type: "dispose" } - * out: { type: "progress", stage: string, progress: number } - * out: { type: "ready" } - * out: { type: "result", text: string } - * out: { type: "error", message: string } - */ - -import { pipeline, env, type AutomaticSpeechRecognitionPipeline } from "@huggingface/transformers"; - -// Disable transformers.js's local-models lookup — we only load from the HF -// CDN so the worker doesn't try to fetch files from our own origin. -env.allowLocalModels = false; -env.allowRemoteModels = true; - -type InitMessage = { type: "init"; model: string }; -type TranscribeMessage = { type: "transcribe"; audio: Float32Array; language?: string }; -type DisposeMessage = { type: "dispose" }; -type InMessage = InitMessage | TranscribeMessage | DisposeMessage; - -type OutMessage = - | { type: "progress"; stage: string; progress: number } - | { type: "ready" } - | { type: "result"; text: string } - | { type: "error"; message: string }; - -const ctx = self as unknown as DedicatedWorkerGlobalScope; - -let asrPipeline: AutomaticSpeechRecognitionPipeline | null = null; -let activeModelId: string | null = null; - -function post(message: OutMessage) { - ctx.postMessage(message); -} - -type ProgressEvent = { - status?: string; - file?: string; - progress?: number; -}; - -async function handleInit(msg: InitMessage) { - if (asrPipeline && activeModelId === msg.model) { - post({ type: "ready" }); - return; - } - if (asrPipeline) { - await asrPipeline.dispose(); - asrPipeline = null; - } - try { - // dtype choice rationale: the `_quantized` / `q8` and `q4` decoder weights - // for whisper-base both contain `MatMulNBits` ops that only execute on - // WebGPU. On browsers without WebGPU (most Firefox, older Chrome) onnxruntime - // throws `Missing required scale: ... weight_merged_0_scale`. fp16 has no - // quantized ops at all so it works on both WASM and WebGPU; it's ~half the - // size of fp32 with no perceptible accuracy loss for ASR. - const created = await pipeline("automatic-speech-recognition", msg.model, { - dtype: { - encoder_model: "fp32", - decoder_model_merged: "fp16", - }, - progress_callback: (e: ProgressEvent) => { - if (typeof e?.progress === "number") { - post({ - type: "progress", - stage: e.status ?? "download", - progress: e.progress, - }); - } - }, - }); - asrPipeline = created as AutomaticSpeechRecognitionPipeline; - activeModelId = msg.model; - post({ type: "ready" }); - } catch (err) { - post({ type: "error", message: errorMessage(err) }); - } -} - -async function handleTranscribe(msg: TranscribeMessage) { - if (!asrPipeline) { - post({ type: "error", message: "Whisper worker not initialized" }); - return; - } - try { - const result = (await asrPipeline(msg.audio, { - language: msg.language && msg.language !== "auto" ? msg.language : undefined, - task: "transcribe", - })) as { text?: string } | Array<{ text?: string }>; - const text = Array.isArray(result) - ? result.map((r) => r.text ?? "").join(" ") - : (result.text ?? ""); - post({ type: "result", text: text.trim() }); - } catch (err) { - post({ type: "error", message: errorMessage(err) }); - } -} - -async function handleDispose() { - if (asrPipeline) { - await asrPipeline.dispose(); - asrPipeline = null; - activeModelId = null; - } -} - -function errorMessage(err: unknown): string { - if (err instanceof Error) return err.message; - return String(err); -} - -ctx.addEventListener("message", (event: MessageEvent) => { - const msg = event.data; - switch (msg.type) { - case "init": - void handleInit(msg); - break; - case "transcribe": - void handleTranscribe(msg); - break; - case "dispose": - void handleDispose(); - break; - } -}); diff --git a/docs/features.md b/docs/features.md index 69dacf18a6..fbc24a34a1 100644 --- a/docs/features.md +++ b/docs/features.md @@ -18,7 +18,7 @@ This page expands the short feature list in the README without turning the READM - **ACP agents:** Kandev supports ACP-native and ACP-adapter agents such as Claude Code, Codex, GitHub Copilot, Gemini CLI, Amp, Auggie, OpenCode, Cursor, Qwen, Factory Droid, iFlow, Kilocode, Pi, Kimi, Kiro, Qoder, Trae, Oh My Pi, Grok, and Hermes. - **Bring-your-own TUI agents:** any agent CLI can run in a PTY terminal through CLI passthrough, even without ACP support. -- **Voice mode:** dictate chat prompts from the composer. Supported engines include browser Web Speech, local in-browser Whisper Web, and server-side Whisper. Settings include language, click-to-toggle or hold-to-talk activation, auto-send, Whisper model size, and keyboard shortcut. +- **Voice mode (plugin):** dictate chat prompts from any composer via the [Voice Mode plugin](https://github.com/kdlbs/kandev-plugin-voice). Engines are browser Web Speech, in-browser Whisper, and an operator-keyed server relay. Settings include language, click-to-toggle or hold-to-talk activation, auto-send, Whisper model size, and keyboard shortcut. Not installed by default. - **Utility agents:** one-shot helpers can generate or improve prompt text, branch names, commit messages, commit descriptions, PR titles, PR descriptions, and session summaries. Users choose a default agent profile, override profiles per action, and add custom utility agents. Profiles own launch settings and unattended permission behavior. - **Custom prompts:** reusable prompts can be created in settings and invoked from chat. - **Secrets:** named secrets can be stored once and reused by profiles or integrations without pasting values into every task. diff --git a/docs/public/configuration.md b/docs/public/configuration.md index 039f3c9395..293c7315a9 100644 --- a/docs/public/configuration.md +++ b/docs/public/configuration.md @@ -119,14 +119,13 @@ The Docker socket is effectively root-equivalent on many hosts. Do not publish i The launcher starts `agentctl`, performs a one-time nonce handshake, and supplies the resulting per-launch token internally. Do not persist or proxy its bootstrap/auth state. Agent command, model, environment, permission, and MCP configuration belongs in agent profiles rather than this section. -### Authentication, Office, Plugins, voice, and feature flags +### Authentication, Office, Plugins, and feature flags | YAML key | Environment variable | Default | Current behavior | |---|---|---|---| | `auth.jwtSecret` | `KANDEV_AUTH_JWTSECRET` | generated value | Accepted and validated compatibility configuration; the current main HTTP product path does not use it as an authentication boundary. | | `auth.tokenDuration` | `KANDEV_AUTH_TOKENDURATION` | `3600` | Must be positive, but is not consumed by the current main HTTP product path. | | `office.jwtSigningKey` | `KANDEV_OFFICE_JWTSIGNINGKEY` | random per start | HMAC key for Office agent-runtime JWTs. Set a stable secret when Office tasks must survive restarts. | -| `voice.openAIApiKey` | `KANDEV_VOICE_OPENAI_API_KEY` | empty | Server-side transcription fallback when browser speech recognition is unavailable. Empty disables the fallback and its endpoint returns unavailable. | | `features.office` | `KANDEV_FEATURES_OFFICE` | `false` in production | Experimental Office UI, routes, services, and automation. | | `features.auth` | `KANDEV_FEATURES_AUTH` | `false` in production | Opt-in authentication and per-user workspaces. The first visitor after enabling completes setup and becomes the admin. | | `features.claude_background_prompt_handoff` | `KANDEV_FEATURES_CLAUDE_BACKGROUND_PROMPT_HANDOFF` | `false` | High-risk experiment that lets Claude Code accept a new prompt after its foreground yields while adapter-attested background work remains active. Other providers keep the coarse busy gate. | @@ -134,7 +133,9 @@ The launcher starts `agentctl`, performs a one-time nonce handshake, and supplie Do not infer security from `auth.jwtSecret`: setting it currently does not turn the local server into an authenticated public service. Office's JWT key has a narrower, active purpose. Store both active secrets and third-party API keys in your deployment secret manager; never commit them in `config.yaml`. -The voice fallback sends audio to the configured OpenAI transcription service and incurs that provider's network, data-handling, and billing behavior. Browser-native speech recognition has its own browser/vendor behavior and does not use this server key. +`voice.openAIApiKey` / `KANDEV_VOICE_OPENAI_API_KEY` was removed. Voice Mode is now the +[Voice Mode plugin](https://github.com/kdlbs/kandev-plugin-voice), and its transcription key lives +in that plugin's own settings. Kandev ignores the old key and no longer serves `/api/v1/transcribe`. ### Logging @@ -286,9 +287,6 @@ debug: office: jwtSigningKey: "" -voice: - openAIApiKey: "" - features: office: false auth: false diff --git a/docs/public/developer-tools.md b/docs/public/developer-tools.md index 3b0a679298..765621610a 100644 --- a/docs/public/developer-tools.md +++ b/docs/public/developer-tools.md @@ -116,37 +116,34 @@ A saved prompt is an instruction, not an authorization or policy boundary. Execu
Voice details -Open **Settings > Task Behavior** (`/settings/preferences/task-behavior`) and expand the Voice Mode -section. Voice Mode inserts a transcript at the cursor in the active chat composer. +Voice Mode is no longer built into Kandev. It ships as the +[Voice Mode plugin](https://github.com/kdlbs/kandev-plugin-voice), installed like any other plugin +from **Settings > Plugins**. Nothing in a Kandev install downloads a speech model, records audio, or +holds a transcription key until you install it. -Defaults are: +Once installed, a microphone button appears beside the existing controls in task chat, Quick Chat, +task creation and new-session creation, on desktop and on a phone. Dictating inserts the transcript +at your cursor; Kandev's own composer still owns the draft and the send. -| Setting | Default | -| ----------------- | ------------------------- | -| Enabled | On | -| Engine | Automatic | -| Language | Auto-detect | -| Activation | Click to toggle | -| Auto-send | Off | -| Whisper Web model | Base, approximately 75 MB | -| Shortcut | `Cmd/Ctrl+Shift+M` | +The plugin offers three engines, chosen per user under **Settings > Plugins > Voice Mode**: -The shortcut is also configurable under **Settings > General > Keyboard Shortcuts**. Hold-to-talk applies on a fine-pointer device. On touch/coarse-pointer devices, Kandev uses toggle behavior while preserving the stored preference. With auto-send enabled, a successful transcript is sent as soon as it is inserted. +| Engine | Where recognition happens | Requirements and data flow | +| --- | --- | --- | +| **Browser speech** | The browser's own recognizer | No audio reaches your Kandev server. Chromium only, and the browser vendor's own handling applies. | +| **In-browser Whisper** | On the device | Downloads and caches an ONNX model from Hugging Face (about 40 MB, 75 MB or 240 MB), then runs locally in a worker. No audio leaves the device. | +| **Server transcription** | Your Kandev server, relaying to OpenAI | Requires an operator to save an OpenAI key in the plugin's settings. The key stays on the server; the request requires a signed-in Kandev user. | -### Choose an engine +`Automatic` picks the first engine the browser can run, in that order. Engine choice is capability +selection, not runtime failover: once recognition starts, an error does not retry through the next +engine. -| Engine | Where recognition happens | Requirements and data flow | -| ------------------ | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| **Automatic** | First available engine | Selects the first currently available capability in this order: Web Speech, Whisper Web, then Whisper Server. A pinned engine that is unavailable is resolved through the same capability order. | -| **Web Speech** | Browser-provided implementation | No audio is sent to the Kandev backend. Browser/vendor behavior and privacy policy still apply, and some implementations require a network service. | -| **Whisper Web** | In the browser | Downloads and caches an ONNX model from Hugging Face, then runs local inference in a worker. Tiny is about 40 MB, Base about 75 MB, and Small about 240 MB. | -| **Whisper Server** | Kandev backend and OpenAI | The browser uploads audio to Kandev; Kandev sends it to OpenAI's `whisper-1` transcription API. Configure `KANDEV_VOICE_OPENAI_API_KEY` on the backend. | +Microphone capture requires browser permission and normally HTTPS or localhost. If recording fails, +check site permission, input device, secure context, model download and cache, and network access. +Switching task or session cancels an in-flight recording and discards its transcript. -Whisper Server accepts at most 10 MiB per request and has a 60-second backend timeout. It returns an unavailable error when the key is not configured, a payload-too-large error above the limit, and an upstream error when transcription fails. Automatic selection can choose this unconfigured server when both browser capabilities are unavailable. - -Engine choice is capability selection, not runtime failover. Once recognition or transcription starts, an error does not retry the request through the next engine. The backend `/api/v1/transcribe` route has no Kandev authentication and spends the configured OpenAI key; protect the whole backend origin and do not publish that endpoint directly. - -Microphone capture requires browser permission and normally HTTPS or localhost. Whisper Web also needs `getUserMedia`, `MediaRecorder`, workers, enough browser storage, and a first-use network download. The UI mentions common Chrome, Edge, and Safari versions, but Kandev does not enforce a browser/version allow-list; runtime availability is determined from the required APIs. If recording fails, check site permission, input device, secure context, model download/cache, browser support, and network access. The composer must remain enabled; switching tasks or disabling the input cancels recording. +Kandev no longer reads `KANDEV_VOICE_OPENAI_API_KEY` and no longer serves `/api/v1/transcribe`. An +operator upgrading from an older release re-enters the key in the plugin's settings, and each user +re-picks their engine and language: preferences are not carried over.
diff --git a/docs/public/feature-status.md b/docs/public/feature-status.md index f346749acd..dba040e932 100644 --- a/docs/public/feature-status.md +++ b/docs/public/feature-status.md @@ -69,7 +69,7 @@ Released builds can lag this page. Check **Settings > System > About** or `kande | Quick Chat | Dependency-bound | Requires an agent profile. Repository-backed chats use isolated Kandev worktrees; closing deletes the ephemeral task permanently. Chats older than seven days are swept at startup and every 24 hours unless their session is `RUNNING` or `IDLE`. | | Utility agents and custom one-shot helpers | Dependency-bound | Require an ACP inference-capable agent and usable model. Passthrough-only agents do not satisfy this path. | | Configuration Chat | Limited | Each workspace currently presents one repository-less configuration conversation. Settings shows it in a tabless floating panel and can move the same session into Quick Chat. Confirmed tab closure deletes its backing task; config-mode tasks are excluded from Quick Chat expiry. | -| Voice input | Dependency-bound | Browser Web Speech, a browser-downloaded Whisper model, and server Whisper paths exist. Selection does not retry another engine after a recognition error. Server Whisper requires `KANDEV_VOICE_OPENAI_API_KEY` (empty by default); without it the transcription route returns 503. | +| Voice input | Plugin | Not built in. Provided by the [Voice Mode plugin](https://github.com/kdlbs/kandev-plugin-voice): browser speech, an in-browser Whisper model, and an operator-keyed server relay. Selection does not retry another engine after a recognition error. | | Embedded VS Code | Limited | code-server runs inside the task runtime with `--auth none` on `0.0.0.0:`. It relies on task-runtime and network isolation rather than its own login. Native Windows Local and Worktree sessions omit it; Linux-backed Local Docker, Sprites, and supported SSH sessions can offer it even when Kandev runs on Windows. The active session executor, not the visitor's browser platform, controls the topbar entry. | | Built-in editors and host editor launch | Dependency-bound | Built-in editing is shipped. External editors depend on host discovery/custom commands. A multi-worktree session presents a repository/worktree picker before host launch; clients that omit the worktree ID retain a first-worktree fallback. A remote executor path may not work without remote-editor configuration. | | Language servers | Limited | Desktop Monaco maps TypeScript/JavaScript, Go, Rust, Python, and experimental Kotlin to task-host servers with diagnostics, completion, hover, navigation, signature help, and semantic tokens. Status distinguishes a launched process awaiting `initialize` from server-reported project work; slow Kotlin Gradle imports can take minutes, with no automatic timeout or universal percentage/ETA. Fine-pointer desktop can place active-file status in the editor toolbar or the opt-in status surface controlled by **Show status bar**; when that surface is off it falls back to the toolbar. Touch tablet uses the toolbar, and phone has no LSP control. Auto-start/installation default off; Kotlin is manual-only. Local PC and Local Docker are supported, while SSH, Sprites, remote Docker, and mobile LSP are not. | diff --git a/docs/public/index.md b/docs/public/index.md index ef8f0be819..c7fb25b2b2 100644 --- a/docs/public/index.md +++ b/docs/public/index.md @@ -29,7 +29,7 @@ Choose the guide that matches the job in front of you: - [Agents and profiles](agents-and-profiles.md): configure agent CLIs, models, modes, permissions, environment, passthrough, and credentials. - [Executors](executors.md): choose local, worktree, Docker, SSH, or Sprites execution and understand the isolation boundary. - [Integrations](integrations.md): configure Azure DevOps, GitHub, GitLab, Jira, Linear, and Sentry for a workspace. -- [Developer tools](developer-tools.md): use quick chat, prompts, utility agents, voice input, editors, terminal, shortcuts, and notifications. +- [Developer tools](developer-tools.md): use quick chat, prompts, utility agents, editors, terminal, shortcuts, and notifications. - [Automation and MCP](automation-and-mcp.md): create scheduled or event-driven work, use task MCP, and connect an external MCP client. - [Feature status](feature-status.md): check support boundaries, dependencies, experimental features, and unfinished work. - [Security and trust](security.md): choose a safe deployment boundary, constrain agent access, protect credentials, and preserve human review. diff --git a/docs/specs/INDEX.md b/docs/specs/INDEX.md index 215296d209..ca21d6e1cd 100644 --- a/docs/specs/INDEX.md +++ b/docs/specs/INDEX.md @@ -47,7 +47,8 @@ Product-wide capabilities that are not tied to a single feature area. | [plugins — authoring experience](plugins/authoring-experience.md) | draft | | [plugins — marketplace](plugins/marketplace.md) | building | | [plugins — agent tools](plugins/agent-tools.md) | draft | -| [plugins — Voice extraction host prerequisites](plugins/voice-extraction-host.md) | draft | +| [plugins — Voice extraction host prerequisites](plugins/voice-extraction-host.md) | shipped | +| [plugins — Voice Mode leaves core](plugins/voice-extraction.md) | shipped | | [semantic-notifications](platform/notifications.md) | shipped | | [workspace-git-status](platform/workspace-git-status.md) | shipped | | [git-subprocess-admission](platform/git-subprocess-admission.md) | building | @@ -193,7 +194,7 @@ Subscription quota tracking and per-agent cheap-model profile routing. | [executor-settings-card-spacing](ui/executor-settings-card-spacing.md) | shipped | | [quick-chat-elevation](ui/quick-chat-elevation.md) | building | | [transcript-navigation-settings](ui/transcript-navigation-settings.md) | shipped | -| [voice-mode-task-behavior](ui/voice-mode-task-behavior.md) | shipped | +| [voice-mode-task-behavior](ui/voice-mode-task-behavior.md) | archived | | [app-status-bar](ui/app-status-bar.md) | shipped | | [quick-terminal](quick-terminal/spec.md) | shipped | | [mobile-task-navigation](ui/mobile-task-navigation.md) | shipped | diff --git a/docs/specs/plugins/voice-extraction-host.md b/docs/specs/plugins/voice-extraction-host.md index 81591ec477..9658d70202 100644 --- a/docs/specs/plugins/voice-extraction-host.md +++ b/docs/specs/plugins/voice-extraction-host.md @@ -1,5 +1,5 @@ --- -status: draft +status: shipped created: 2026-08-11 owner: kandev --- @@ -51,7 +51,8 @@ Decision: [ADR-2026-08-11-composer-access-authenticated-webhooks](../../decision - Disabling, uninstalling, or reloading a plugin aborts its in-flight webhook requests, unregisters its composer actions, and leaves native drafts intact. - The current core Voice Mode stays present until a separately delivered Voice plugin proves parity - on all listed surfaces. Removing core Voice Mode is not part of these host prerequisites. + on all listed surfaces. Removing core Voice Mode is not part of these host prerequisites. That + removal has since happened; see [Voice Mode leaves core](voice-extraction.md). ## API Surface diff --git a/docs/specs/plugins/voice-extraction.md b/docs/specs/plugins/voice-extraction.md new file mode 100644 index 0000000000..f662d70f7d --- /dev/null +++ b/docs/specs/plugins/voice-extraction.md @@ -0,0 +1,87 @@ +--- +status: shipped +created: 2026-08-12 +owner: kandev +--- + +# Voice Mode Leaves Core + +## Why + +Voice Mode is an optional way to put text into a composer. It is not part of what kandev is for, and +carrying it in core cost every install a 100 MB browser ML dependency, a per-user settings block in +the shared user-settings row, an unauthenticated transcription route, and a server-side OpenAI key +whether or not anyone dictated a word. + +The host prerequisites for moving it out shipped separately +([voice-extraction-host](voice-extraction-host.md), ADR +[composer access and authenticated webhooks](../../decisions/2026-08-11-composer-access-authenticated-webhooks.md)). +This spec records the extraction itself: what the plugin owns, what core stops owning, and what an +existing user has to do. + +## What + +- Voice Mode ships as [`kdlbs/kandev-plugin-voice`](https://github.com/kdlbs/kandev-plugin-voice), + installed like any other plugin. Core contains no voice code. +- The plugin reaches behaviour parity with the removed feature: the same three engines (browser + speech, in-browser Whisper, server-side Whisper), insertion at the current selection with the same + word-boundary spacing rule, optional auto-send, hold and toggle activation with the coarse-pointer + override, cancellation on task/session change, the same `Cmd/Ctrl+Shift+M` shortcut, and the same + four composers on desktop and phone: task chat, Quick Chat, task creation, new-session creation. +- The plugin uses only published host contracts: the composer action slots, `host.storage` for + per-user preferences, the owner-scoped `plugin-settings` slot for its settings form, + `registerKeybinding` for the shortcut, the `/api/plugins/{id}/ui/*` asset route for its Whisper worker, and one authenticated + webhook for the server relay. It adds no core-specific coupling. +- Core keeps every generic host API the extraction needed. None of them mention voice. + +### Removed from core + +| Removed | Replacement | +|---|---| +| `POST /api/v1/transcribe` (unauthenticated) | the plugin's `transcribe` webhook, which requires a signed-in user | +| `voice.openAIApiKey` / `KANDEV_VOICE_OPENAI_API_KEY` | the plugin's own **OpenAI API key** setting | +| `userSettings.voiceMode` (backend model, DTO, store, boot payload, frontend types) | the plugin's per-user `host.storage` entry | +| The Voice Mode section of **Settings > Task Behavior** | **Settings > Plugins > Voice Mode** | +| The `VOICE_INPUT_TOGGLE` core shortcut | the plugin keybinding, listed and rebindable in the same **Settings > Keyboard shortcuts** page | +| `@huggingface/transformers` in `apps/web` | bundled inside the plugin's Whisper worker | + +### Migration + +Both migration steps are manual and deliberate; neither is silent. + +- **The OpenAI key does not move.** An operator who set `KANDEV_VOICE_OPENAI_API_KEY` re-enters the + key in the plugin's settings form. Core never had a way to write into a plugin's secret storage + and should not gain one for a single plugin. +- **Per-user preferences do not move.** Everyone's engine, language, model, activation mode and + auto-send return to the plugin's defaults, which match the previous core defaults except that the + engine list now hides server transcription until an operator saves a key. Copying them across + would require core to write into one named plugin's per-user storage, which is exactly the + coupling the extraction exists to remove. +- A stale `voice_mode` object left inside an existing `users.settings` row is inert. Nothing reads + it and nothing needs to delete it. + +## Scenarios + +- **GIVEN** kandev with no plugins installed, **WHEN** a user opens task chat, **THEN** no + microphone control appears and nothing in Settings mentions voice. +- **GIVEN** the Voice plugin installed and enabled, **WHEN** a user dictates into task chat, Quick + Chat, task creation or new-session creation on desktop or a phone, **THEN** the transcript is + inserted at the caret and the native submit path owns sending it. +- **GIVEN** no operator OpenAI key, **WHEN** a user opens the plugin's settings, **THEN** server + transcription is shown as unavailable and the two browser engines still work. +- **GIVEN** an operator key saved in the plugin, **WHEN** a signed-in user dictates with the server + engine, **THEN** the audio reaches OpenAI through the plugin's authenticated webhook and the key + never reaches the browser. +- **GIVEN** an anonymous caller on an install with authentication enabled, **WHEN** it posts to the + transcription webhook, **THEN** the request fails before the plugin runs and cannot spend the + operator's budget. +- **GIVEN** a user who had Voice Mode configured before upgrading, **WHEN** they install the plugin, + **THEN** dictation works at the plugin's defaults and their previous engine and language choices + are not carried over. + +## Out Of Scope + +- Publishing the plugin to the official marketplace catalog. +- Localizing the plugin. Kandev has no host i18n API for plugins yet; the plugin keeps its copy in + one module so a catalog can be added once that API exists. +- Migrating operator or per-user configuration automatically, for the reasons above. diff --git a/docs/specs/ui/voice-mode-task-behavior.md b/docs/specs/ui/voice-mode-task-behavior.md index e58a4507fa..c0c76b7109 100644 --- a/docs/specs/ui/voice-mode-task-behavior.md +++ b/docs/specs/ui/voice-mode-task-behavior.md @@ -1,11 +1,16 @@ --- -status: shipped +status: archived created: 2026-08-11 owner: kandev --- # Voice Mode In Task Behavior +> **Archived.** Voice Mode left core and now ships as +> [`kdlbs/kandev-plugin-voice`](https://github.com/kdlbs/kandev-plugin-voice); its settings live on +> the plugin's own page. See [Voice Mode leaves core](../plugins/voice-extraction.md). This spec is +> kept as the record of how the setting was placed while it was a core feature. + ## Why Voice input changes how users interact with tasks, but its controls currently occupy a separate