@sirenodeck/sirenodeck 0.1.1-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +69 -0
- package/bin/_wrapper-shared.js +42 -0
- package/bin/dev.js +95 -0
- package/bin/sirenodeck.js +42 -0
- package/config/default-config.yml +117 -0
- package/config/starter-config.yml +54 -0
- package/dist/main.mjs +64702 -0
- package/package.json +140 -0
- package/src/action/executor.ts +156 -0
- package/src/action/index.ts +7 -0
- package/src/addon/api-types.ts +15 -0
- package/src/addon/api.ts +461 -0
- package/src/addon/check-runner.ts +84 -0
- package/src/addon/index.ts +5 -0
- package/src/addon/loader.ts +441 -0
- package/src/addon/registry.ts +230 -0
- package/src/addon/spec.ts +69 -0
- package/src/api/addon.ts +33 -0
- package/src/api/asset-url.ts +24 -0
- package/src/api/index.ts +50 -0
- package/src/api/protocol-internal.ts +656 -0
- package/src/api/protocol.ts +2 -0
- package/src/api/react/index.ts +11 -0
- package/src/api/react/registry.ts +91 -0
- package/src/api/react/use-addon-channel.ts +26 -0
- package/src/api/react/use-button-state.tsx +23 -0
- package/src/api/react/use-deck.ts +24 -0
- package/src/assets/logo.png +0 -0
- package/src/assets/logo72x72.png +0 -0
- package/src/assets/logoFull.png +0 -0
- package/src/builtin-addons/brightness/README.md +51 -0
- package/src/builtin-addons/brightness/buttons/brightness/backend.ts +13 -0
- package/src/builtin-addons/brightness/buttons/brightness/config.ts +10 -0
- package/src/builtin-addons/brightness/buttons/brightness/frontend.tsx +50 -0
- package/src/builtin-addons/brightness/domain/macos.ts +46 -0
- package/src/builtin-addons/brightness/index.d.ts +5 -0
- package/src/builtin-addons/brightness/index.ts +20 -0
- package/src/builtin-addons/brightness/poller.ts +27 -0
- package/src/builtin-addons/brightness/providers/darwin.ts +107 -0
- package/src/builtin-addons/brightness/providers/index.ts +81 -0
- package/src/builtin-addons/brightness/providers/linux.ts +108 -0
- package/src/builtin-addons/brightness/providers/windows.ts +77 -0
- package/src/builtin-addons/brightness/sirenodeck.json +6 -0
- package/src/builtin-addons/coding-agents/assets/claude-code.svg +1 -0
- package/src/builtin-addons/coding-agents/assets/opencode-dark-square.svg +1 -0
- package/src/builtin-addons/coding-agents/buttons/agent/backend.ts +82 -0
- package/src/builtin-addons/coding-agents/buttons/agent/config.ts +13 -0
- package/src/builtin-addons/coding-agents/buttons/agent/frontend.tsx +130 -0
- package/src/builtin-addons/coding-agents/buttons/agent/pages.tsx +150 -0
- package/src/builtin-addons/coding-agents/buttons/summary/backend.ts +48 -0
- package/src/builtin-addons/coding-agents/buttons/summary/config.ts +11 -0
- package/src/builtin-addons/coding-agents/buttons/summary/frontend.tsx +254 -0
- package/src/builtin-addons/coding-agents/decks/agents.ts +69 -0
- package/src/builtin-addons/coding-agents/global/backend.ts +402 -0
- package/src/builtin-addons/coding-agents/global-entry.ts +7 -0
- package/src/builtin-addons/coding-agents/hook/script.ts +123 -0
- package/src/builtin-addons/coding-agents/index.ts +15 -0
- package/src/builtin-addons/coding-agents/manifest.ts +115 -0
- package/src/builtin-addons/coding-agents/providers/claude-code.ts +470 -0
- package/src/builtin-addons/coding-agents/providers/claude-instances.ts +184 -0
- package/src/builtin-addons/coding-agents/providers/opencode-instances.ts +108 -0
- package/src/builtin-addons/coding-agents/providers/opencode.ts +636 -0
- package/src/builtin-addons/coding-agents/providers/registry.ts +120 -0
- package/src/builtin-addons/coding-agents/providers/spawn.ts +112 -0
- package/src/builtin-addons/coding-agents/shared/claude-hook-events.ts +55 -0
- package/src/builtin-addons/coding-agents/shared/claude-status.ts +236 -0
- package/src/builtin-addons/coding-agents/shared/live-count.ts +39 -0
- package/src/builtin-addons/coding-agents/shared/notifier.ts +58 -0
- package/src/builtin-addons/coding-agents/shared/opencode-status.ts +82 -0
- package/src/builtin-addons/coding-agents/shared/snapshot.ts +66 -0
- package/src/builtin-addons/coding-agents/shared/state.ts +72 -0
- package/src/builtin-addons/coding-agents/sirenodeck.json +6 -0
- package/src/builtin-addons/coding-agents/types/types.ts +33 -0
- package/src/builtin-addons/core/README.md +89 -0
- package/src/builtin-addons/core/buttons/action/backend.ts +7 -0
- package/src/builtin-addons/core/buttons/action/config.ts +13 -0
- package/src/builtin-addons/core/buttons/action/frontend.tsx +16 -0
- package/src/builtin-addons/core/buttons/change-deck/backend.ts +18 -0
- package/src/builtin-addons/core/buttons/change-deck/config.ts +15 -0
- package/src/builtin-addons/core/buttons/change-deck/frontend.tsx +18 -0
- package/src/builtin-addons/core/buttons/media-sample/backend.ts +8 -0
- package/src/builtin-addons/core/buttons/media-sample/config.ts +7 -0
- package/src/builtin-addons/core/buttons/media-sample/frontend.tsx +2 -0
- package/src/builtin-addons/core/buttons/page-nav/backend.ts +28 -0
- package/src/builtin-addons/core/buttons/page-nav/config.ts +9 -0
- package/src/builtin-addons/core/buttons/page-nav/frontend.tsx +68 -0
- package/src/builtin-addons/core/buttons/toggle/backend.ts +119 -0
- package/src/builtin-addons/core/buttons/toggle/config.ts +57 -0
- package/src/builtin-addons/core/buttons/toggle/frontend.tsx +57 -0
- package/src/builtin-addons/core/buttons/toggle/global-service.ts +174 -0
- package/src/builtin-addons/core/index.ts +120 -0
- package/src/builtin-addons/core/sirenodeck.json +6 -0
- package/src/builtin-addons/date-time/README.md +73 -0
- package/src/builtin-addons/date-time/buttons/analog-clock/backend.ts +10 -0
- package/src/builtin-addons/date-time/buttons/analog-clock/config.ts +4 -0
- package/src/builtin-addons/date-time/buttons/analog-clock/frontend.tsx +88 -0
- package/src/builtin-addons/date-time/buttons/custom/backend.ts +10 -0
- package/src/builtin-addons/date-time/buttons/custom/config.ts +9 -0
- package/src/builtin-addons/date-time/buttons/custom/frontend.tsx +26 -0
- package/src/builtin-addons/date-time/buttons/date/backend.ts +10 -0
- package/src/builtin-addons/date-time/buttons/date/config.ts +10 -0
- package/src/builtin-addons/date-time/buttons/date/frontend.tsx +42 -0
- package/src/builtin-addons/date-time/buttons/time/backend.ts +10 -0
- package/src/builtin-addons/date-time/buttons/time/config.ts +9 -0
- package/src/builtin-addons/date-time/buttons/time/frontend.tsx +29 -0
- package/src/builtin-addons/date-time/index.d.ts +5 -0
- package/src/builtin-addons/date-time/index.ts +30 -0
- package/src/builtin-addons/date-time/poller.ts +11 -0
- package/src/builtin-addons/date-time/shared/format-date.ts +36 -0
- package/src/builtin-addons/date-time/shared/format.ts +105 -0
- package/src/builtin-addons/date-time/shared/use-now.ts +10 -0
- package/src/builtin-addons/date-time/sirenodeck.json +6 -0
- package/src/builtin-addons/emoji-selector/README.md +45 -0
- package/src/builtin-addons/emoji-selector/assets/launcher.svg +10 -0
- package/src/builtin-addons/emoji-selector/assets/smileys.svg +8 -0
- package/src/builtin-addons/emoji-selector/buttons/category/backend.ts +14 -0
- package/src/builtin-addons/emoji-selector/buttons/category/config.ts +8 -0
- package/src/builtin-addons/emoji-selector/buttons/category/frontend.tsx +19 -0
- package/src/builtin-addons/emoji-selector/buttons/emoji/backend.ts +7 -0
- package/src/builtin-addons/emoji-selector/buttons/emoji/config.ts +9 -0
- package/src/builtin-addons/emoji-selector/buttons/emoji/frontend.tsx +11 -0
- package/src/builtin-addons/emoji-selector/buttons/launcher/backend.ts +13 -0
- package/src/builtin-addons/emoji-selector/buttons/launcher/config.ts +6 -0
- package/src/builtin-addons/emoji-selector/buttons/launcher/frontend.tsx +31 -0
- package/src/builtin-addons/emoji-selector/data/categories.ts +1084 -0
- package/src/builtin-addons/emoji-selector/decks/index.ts +137 -0
- package/src/builtin-addons/emoji-selector/index.d.ts +5 -0
- package/src/builtin-addons/emoji-selector/index.ts +29 -0
- package/src/builtin-addons/emoji-selector/sirenodeck.json +6 -0
- package/src/builtin-addons/emoji-selector/support.ts +49 -0
- package/src/builtin-addons/index.ts +13 -0
- package/src/builtin-addons/internal-settings/README.md +26 -0
- package/src/builtin-addons/internal-settings/backend.ts +58 -0
- package/src/builtin-addons/internal-settings/buttons/app-info/backend.ts +7 -0
- package/src/builtin-addons/internal-settings/buttons/app-info/config.ts +13 -0
- package/src/builtin-addons/internal-settings/buttons/app-info/frontend.tsx +18 -0
- package/src/builtin-addons/internal-settings/buttons/brightness/common/frontend.tsx +50 -0
- package/src/builtin-addons/internal-settings/buttons/brightness-down/backend.ts +16 -0
- package/src/builtin-addons/internal-settings/buttons/brightness-down/config.ts +6 -0
- package/src/builtin-addons/internal-settings/buttons/brightness-down/frontend.tsx +8 -0
- package/src/builtin-addons/internal-settings/buttons/brightness-up/backend.ts +16 -0
- package/src/builtin-addons/internal-settings/buttons/brightness-up/config.ts +6 -0
- package/src/builtin-addons/internal-settings/buttons/brightness-up/frontend.tsx +8 -0
- package/src/builtin-addons/internal-settings/decks/settings.ts +25 -0
- package/src/builtin-addons/internal-settings/index.d.ts +4 -0
- package/src/builtin-addons/internal-settings/index.ts +53 -0
- package/src/builtin-addons/internal-settings/logo72x72.png +0 -0
- package/src/builtin-addons/internal-settings/sirenodeck.json +6 -0
- package/src/builtin-addons/media/README.md +45 -0
- package/src/builtin-addons/media/backend.ts +143 -0
- package/src/builtin-addons/media/buttons/media-player/backend.ts +19 -0
- package/src/builtin-addons/media/buttons/media-player/components/MediaStatus.tsx +25 -0
- package/src/builtin-addons/media/buttons/media-player/components/MediaSurface.tsx +52 -0
- package/src/builtin-addons/media/buttons/media-player/components/ProgressBar.tsx +21 -0
- package/src/builtin-addons/media/buttons/media-player/components/status-meta.ts +36 -0
- package/src/builtin-addons/media/buttons/media-player/config.ts +4 -0
- package/src/builtin-addons/media/buttons/media-player/frontend.tsx +29 -0
- package/src/builtin-addons/media/buttons/volume/common/frontend.tsx +52 -0
- package/src/builtin-addons/media/buttons/volume/mute/backend.ts +13 -0
- package/src/builtin-addons/media/buttons/volume/mute/config.ts +4 -0
- package/src/builtin-addons/media/buttons/volume/mute/frontend.tsx +26 -0
- package/src/builtin-addons/media/buttons/volume/volume-down/backend.ts +14 -0
- package/src/builtin-addons/media/buttons/volume/volume-down/config.ts +6 -0
- package/src/builtin-addons/media/buttons/volume/volume-down/frontend.tsx +10 -0
- package/src/builtin-addons/media/buttons/volume/volume-up/backend.ts +14 -0
- package/src/builtin-addons/media/buttons/volume/volume-up/config.ts +6 -0
- package/src/builtin-addons/media/buttons/volume/volume-up/frontend.tsx +8 -0
- package/src/builtin-addons/media/checks.ts +104 -0
- package/src/builtin-addons/media/executor.ts +36 -0
- package/src/builtin-addons/media/index.d.ts +5 -0
- package/src/builtin-addons/media/index.ts +53 -0
- package/src/builtin-addons/media/progress.ts +16 -0
- package/src/builtin-addons/media/providers/darwin.ts +391 -0
- package/src/builtin-addons/media/providers/index.ts +27 -0
- package/src/builtin-addons/media/providers/linux.ts +196 -0
- package/src/builtin-addons/media/providers/types.ts +48 -0
- package/src/builtin-addons/media/providers/windows.ts +121 -0
- package/src/builtin-addons/media/sirenodeck.json +6 -0
- package/src/builtin-addons/media/state.ts +19 -0
- package/src/builtin-addons/register-builtins.ts +65 -0
- package/src/builtin-addons/session/README.md +14 -0
- package/src/builtin-addons/session/buttons/session-info/backend.ts +8 -0
- package/src/builtin-addons/session/buttons/session-info/config.ts +4 -0
- package/src/builtin-addons/session/buttons/session-info/frontend.tsx +2 -0
- package/src/builtin-addons/session/buttons/time/backend.ts +7 -0
- package/src/builtin-addons/session/buttons/time/config.ts +6 -0
- package/src/builtin-addons/session/buttons/time/frontend.tsx +2 -0
- package/src/builtin-addons/session/index.d.ts +5 -0
- package/src/builtin-addons/session/index.ts +18 -0
- package/src/builtin-addons/session/sirenodeck.json +6 -0
- package/src/builtin-addons/system-status/README.md +169 -0
- package/src/builtin-addons/system-status/buttons/_shared.ts +131 -0
- package/src/builtin-addons/system-status/buttons/system-status/frontend.tsx +224 -0
- package/src/builtin-addons/system-status/buttons/system-status/schemas.ts +67 -0
- package/src/builtin-addons/system-status/domain/catalog.ts +53 -0
- package/src/builtin-addons/system-status/domain/chart-sampler.ts +58 -0
- package/src/builtin-addons/system-status/domain/display-metrics.ts +128 -0
- package/src/builtin-addons/system-status/domain/index.ts +10 -0
- package/src/builtin-addons/system-status/domain/live-metrics.ts +1021 -0
- package/src/builtin-addons/system-status/domain/metric-ids.ts +0 -0
- package/src/builtin-addons/system-status/index.ts +16 -0
- package/src/builtin-addons/system-status/manifest.ts +105 -0
- package/src/builtin-addons/system-status/shared/metrics-catalog.ts +316 -0
- package/src/builtin-addons/system-status/sirenodeck.json +6 -0
- package/src/builtin-addons/test-buildin/decks/action-buttons-test.ts +56 -0
- package/src/builtin-addons/test-buildin/index.d.ts +5 -0
- package/src/builtin-addons/test-buildin/index.ts +16 -0
- package/src/builtin-addons/value-display/README.md +59 -0
- package/src/builtin-addons/value-display/backend.ts +86 -0
- package/src/builtin-addons/value-display/buttons/value-display/backend.ts +17 -0
- package/src/builtin-addons/value-display/buttons/value-display/config.ts +36 -0
- package/src/builtin-addons/value-display/buttons/value-display/frontend.tsx +54 -0
- package/src/builtin-addons/value-display/index.d.ts +5 -0
- package/src/builtin-addons/value-display/index.ts +23 -0
- package/src/builtin-addons/value-display/sirenodeck.json +6 -0
- package/src/builtin-addons/weather/README.md +46 -0
- package/src/builtin-addons/weather/backend.ts +121 -0
- package/src/builtin-addons/weather/buttons/weather/backend.ts +27 -0
- package/src/builtin-addons/weather/buttons/weather/config.ts +29 -0
- package/src/builtin-addons/weather/buttons/weather/frontend.tsx +63 -0
- package/src/builtin-addons/weather/buttons/weather/icons.tsx +88 -0
- package/src/builtin-addons/weather/buttons/weather/pages.tsx +109 -0
- package/src/builtin-addons/weather/domain/codes.ts +35 -0
- package/src/builtin-addons/weather/domain/fetch.ts +47 -0
- package/src/builtin-addons/weather/index.d.ts +5 -0
- package/src/builtin-addons/weather/index.ts +23 -0
- package/src/builtin-addons/weather/poller.ts +70 -0
- package/src/builtin-addons/weather/provider/city-key.ts +9 -0
- package/src/builtin-addons/weather/provider/codes.ts +35 -0
- package/src/builtin-addons/weather/provider/fetch.ts +97 -0
- package/src/builtin-addons/weather/provider/geocode.ts +67 -0
- package/src/builtin-addons/weather/provider/types.ts +42 -0
- package/src/builtin-addons/weather/sirenodeck.json +6 -0
- package/src/cli/coding-agents-onboarding.ts +434 -0
- package/src/cli/commands/addon-decks.ts +441 -0
- package/src/cli/commands/addon-registry.ts +639 -0
- package/src/cli/commands/emulator-mode.ts +289 -0
- package/src/cli/commands/install.ts +128 -0
- package/src/cli/commands/logs.ts +74 -0
- package/src/cli/commands/network-bind.ts +111 -0
- package/src/cli/commands/pipeline/helpers.ts +75 -0
- package/src/cli/commands/pipeline/preflight.ts +20 -0
- package/src/cli/commands/port-identity.ts +152 -0
- package/src/cli/commands/reload.ts +37 -0
- package/src/cli/commands/restart.ts +139 -0
- package/src/cli/commands/run.ts +2270 -0
- package/src/cli/commands/service/install.ts +121 -0
- package/src/cli/commands/service/render-template.ts +158 -0
- package/src/cli/commands/service-manager.ts +184 -0
- package/src/cli/commands/spawn-daemon.ts +251 -0
- package/src/cli/commands/start.ts +1061 -0
- package/src/cli/commands/status.ts +110 -0
- package/src/cli/commands/stop.ts +121 -0
- package/src/cli/commands/subprocess-supervisor.ts +203 -0
- package/src/cli/commands/system-requirements.ts +699 -0
- package/src/cli/cwd.ts +28 -0
- package/src/cli/first-run.ts +149 -0
- package/src/cli/http-server.ts +244 -0
- package/src/cli/index.ts +355 -0
- package/src/cli/install-root.ts +26 -0
- package/src/cli/main.ts +194 -0
- package/src/cli/package-manager.ts +175 -0
- package/src/cli/probe-deps.ts +45 -0
- package/src/cli/prompt.ts +22 -0
- package/src/cli/startup-display.ts +452 -0
- package/src/config/builtin-icons.ts +57 -0
- package/src/config/config-diff.ts +54 -0
- package/src/config/discovery.ts +64 -0
- package/src/config/icon-resolver.ts +90 -0
- package/src/config/include-resolver.ts +202 -0
- package/src/config/index.ts +9 -0
- package/src/config/loader.ts +187 -0
- package/src/config/mutation.ts +514 -0
- package/src/config/reference-expander.ts +84 -0
- package/src/config/schemas.ts +223 -0
- package/src/config/validation.ts +254 -0
- package/src/core/asset-id.ts +21 -0
- package/src/core/gesture-state.ts +344 -0
- package/src/core/icon-asset-registry.ts +127 -0
- package/src/core/icon-source.ts +86 -0
- package/src/core/index.ts +30 -0
- package/src/core/mime.ts +16 -0
- package/src/core/pagination.ts +90 -0
- package/src/core/pub-sub.ts +126 -0
- package/src/core/settings.ts +153 -0
- package/src/core/store.ts +88 -0
- package/src/core/watcher.ts +73 -0
- package/src/deck/addon-handler-bridge.ts +516 -0
- package/src/deck/deck-config.ts +271 -0
- package/src/deck/editor-surfaces.ts +87 -0
- package/src/deck/host-context.ts +18 -0
- package/src/deck/index.ts +102 -0
- package/src/deck/macro-parse.ts +182 -0
- package/src/deck/methods.ts +348 -0
- package/src/deck/paginate-deck.ts +98 -0
- package/src/deck/position-buttons.ts +57 -0
- package/src/deck/runtime/index.ts +5 -0
- package/src/deck/runtime/runtime.ts +1060 -0
- package/src/deck/runtime-subscriptions.ts +45 -0
- package/src/deck/system-back-injection.ts +62 -0
- package/src/deck/system-buttons/registry.tsx +97 -0
- package/src/deck/system-buttons/types.ts +14 -0
- package/src/device/black-frame.ts +73 -0
- package/src/device/index.ts +13 -0
- package/src/device/linux-udev.ts +36 -0
- package/src/device/models.ts +89 -0
- package/src/device/reconnecting.ts +193 -0
- package/src/device/registry.ts +38 -0
- package/src/device/stream-deck.ts +155 -0
- package/src/index.ts +74 -0
- package/src/outputClient/emulator.ts +357 -0
- package/src/outputClient/index.ts +26 -0
- package/src/outputClient/open-browser.ts +26 -0
- package/src/outputClient/real.ts +393 -0
- package/src/outputClient/types.ts +83 -0
- package/src/render/browser-renderer.ts +240 -0
- package/src/render/buffer-hash.ts +20 -0
- package/src/render/editor-handler.ts +197 -0
- package/src/render/emulator-server.ts +128 -0
- package/src/render/icon-source-resolver.ts +69 -0
- package/src/render/index.ts +16 -0
- package/src/render/protocol.ts +61 -0
- package/src/render/push-raw-image.ts +100 -0
- package/src/render/screenshot-cadence.ts +87 -0
- package/src/render/state-publisher.ts +110 -0
- package/src/render/vite-server.ts +138 -0
- package/src/render/ws-bridge.ts +315 -0
- package/src/system/device-selection.ts +80 -0
- package/src/system/glob-match.ts +74 -0
- package/src/system/providers/active-app/darwin.ts +117 -0
- package/src/system/providers/active-app/linux.ts +245 -0
- package/src/system/providers/active-app/wayland-gnome.ts +303 -0
- package/src/system/providers/active-app/windows.ts +94 -0
- package/src/system/providers/active-app.ts +97 -0
- package/src/system/providers/clipboard/darwin.ts +65 -0
- package/src/system/providers/clipboard/linux.ts +163 -0
- package/src/system/providers/clipboard/windows.ts +94 -0
- package/src/system/providers/clipboard.ts +84 -0
- package/src/system/providers/error.ts +16 -0
- package/src/system/providers/key-macro/darwin.ts +221 -0
- package/src/system/providers/key-macro/linux.ts +651 -0
- package/src/system/providers/key-macro/parser.ts +215 -0
- package/src/system/providers/key-macro/windows.ts +476 -0
- package/src/system/providers/key-macro.ts +79 -0
- package/src/system/providers/notification/darwin.ts +36 -0
- package/src/system/providers/notification/linux.ts +115 -0
- package/src/system/providers/notification/windows.ts +72 -0
- package/src/system/providers/notification.ts +75 -0
- package/src/system/providers/session/darwin.ts +82 -0
- package/src/system/providers/session/linux.ts +175 -0
- package/src/system/providers/session/windows.ts +73 -0
- package/src/system/providers/session.ts +89 -0
- package/src/system/providers/shared.ts +65 -0
- package/src/system/requirements.ts +235 -0
- package/src/system/setup-wizard/config-seed.ts +30 -0
- package/src/system/setup-wizard/format.ts +275 -0
- package/src/system/setup-wizard/index.ts +45 -0
- package/src/system/setup-wizard/open-settings.ts +26 -0
- package/src/system/setup-wizard/plan.ts +230 -0
- package/src/system/setup-wizard/probe-cache.ts +34 -0
- package/src/system/setup-wizard/probe.ts +486 -0
- package/src/system/setup-wizard/runtime-features.ts +85 -0
- package/src/system/setup-wizard/sudo.ts +178 -0
- package/src/system/setup-wizard/types.ts +133 -0
- package/src/system/virtual-stream-deck.ts +67 -0
- package/src/themes/css.ts +210 -0
- package/src/themes/default/assets/IBMPlexSans-Italic-VariableFont_wdth,wght.ttf +0 -0
- package/src/themes/default/assets/IBMPlexSans-VariableFont_wdth,wght.ttf +0 -0
- package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-Bold.ttf +0 -0
- package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-BoldItalic.ttf +0 -0
- package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-ExtraLight.ttf +0 -0
- package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-ExtraLightItalic.ttf +0 -0
- package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-Italic.ttf +0 -0
- package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-Light.ttf +0 -0
- package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-LightItalic.ttf +0 -0
- package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-Medium.ttf +0 -0
- package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-MediumItalic.ttf +0 -0
- package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-Regular.ttf +0 -0
- package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-SemiBold.ttf +0 -0
- package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-SemiBoldItalic.ttf +0 -0
- package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-Thin.ttf +0 -0
- package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-ThinItalic.ttf +0 -0
- package/src/themes/default/assets/IBM_Plex_Mono/OFL.txt +93 -0
- package/src/themes/default/assets/OFL.txt +93 -0
- package/src/themes/default/assets/README.txt +106 -0
- package/src/themes/default/assets/static/IBMPlexSans-Bold.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans-BoldItalic.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans-ExtraLight.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans-ExtraLightItalic.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans-Italic.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans-Light.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans-LightItalic.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans-Medium.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans-MediumItalic.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans-Regular.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans-SemiBold.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans-SemiBoldItalic.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans-Thin.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans-ThinItalic.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_Condensed-Bold.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_Condensed-BoldItalic.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_Condensed-ExtraLight.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_Condensed-ExtraLightItalic.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_Condensed-Italic.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_Condensed-Light.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_Condensed-LightItalic.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_Condensed-Medium.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_Condensed-MediumItalic.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_Condensed-Regular.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_Condensed-SemiBold.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_Condensed-SemiBoldItalic.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_Condensed-Thin.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_Condensed-ThinItalic.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-Bold.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-BoldItalic.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-ExtraLight.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-ExtraLightItalic.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-Italic.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-Light.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-LightItalic.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-Medium.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-MediumItalic.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-Regular.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-SemiBold.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-SemiBoldItalic.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-Thin.ttf +0 -0
- package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-ThinItalic.ttf +0 -0
- package/src/themes/default/components.css +81 -0
- package/src/themes/default/sirenodeck.json +170 -0
- package/src/themes/index.ts +14 -0
- package/src/themes/light/assets/IBMPlexSans-Italic-VariableFont_wdth,wght.ttf +0 -0
- package/src/themes/light/assets/IBMPlexSans-VariableFont_wdth,wght.ttf +0 -0
- package/src/themes/light/assets/IBM_Plex_Mono/IBMPlexMono-Bold.ttf +0 -0
- package/src/themes/light/assets/IBM_Plex_Mono/IBMPlexMono-Medium.ttf +0 -0
- package/src/themes/light/assets/IBM_Plex_Mono/IBMPlexMono-Regular.ttf +0 -0
- package/src/themes/light/assets/IBM_Plex_Mono/OFL.txt +93 -0
- package/src/themes/light/assets/OFL.txt +93 -0
- package/src/themes/light/assets/README.txt +106 -0
- package/src/themes/light/assets/static/IBMPlexSans-Light.ttf +0 -0
- package/src/themes/light/assets/static/IBMPlexSans-LightItalic.ttf +0 -0
- package/src/themes/light/assets/static/IBMPlexSans-Thin.ttf +0 -0
- package/src/themes/light/components.css +70 -0
- package/src/themes/light/sirenodeck.json +170 -0
- package/src/themes/loader.ts +343 -0
- package/src/themes/manifest.ts +164 -0
- package/src/themes/use-resolved-theme.tsx +32 -0
- package/src/ui/ButtonFrame.tsx +230 -0
- package/src/ui/contexts/AssetCacheContext.tsx +43 -0
- package/src/ui/index.ts +20 -0
- package/src/ui/primitives/Chip.tsx +53 -0
- package/src/ui/primitives/Icon.tsx +209 -0
- package/src/ui/primitives/Label.tsx +82 -0
- package/src/ui/primitives/ProgressBar.tsx +38 -0
- package/src/ui/primitives/TapIndicator.tsx +67 -0
- package/src/ui/primitives/Text.tsx +810 -0
- package/src/ui/primitives/index.ts +6 -0
- package/src/ui/surfaces/BarsSurface.tsx +211 -0
- package/src/ui/surfaces/IconLabelProgressSurface.tsx +133 -0
- package/src/ui/surfaces/IconLabelSurface.tsx +59 -0
- package/src/ui/surfaces/LabelValueListSurface.tsx +202 -0
- package/src/ui/surfaces/PaginatedSurface.tsx +164 -0
- package/src/ui/surfaces/SplitActionSurface.tsx +98 -0
- package/src/ui/surfaces/TemporaryErrorSurface.tsx +68 -0
- package/src/ui/surfaces/ValueChart.tsx +218 -0
- package/src/ui/surfaces/index.ts +32 -0
- package/src/ui/surfaces/paginated-schema.ts +22 -0
- package/src/ui/theme-presentation-builder.ts +184 -0
- package/src/ui/theme-presentation.tsx +146 -0
- package/src/ui/utils/cn.ts +12 -0
- package/src/util/cache-paths.ts +38 -0
- package/src/util/config-diff.ts +16 -0
- package/src/util/daemon-control.ts +85 -0
- package/src/util/daemon.ts +575 -0
- package/src/util/device-config.ts +56 -0
- package/src/util/errors.ts +13 -0
- package/src/util/log-reader.ts +114 -0
- package/src/util/log-tail.ts +103 -0
- package/src/util/logger.ts +417 -0
- package/src/util/process-title.ts +79 -0
- package/src/util/single-instance.ts +202 -0
- package/src/version.ts +10 -0
- package/src/vite/index.ts +6 -0
- package/src/vite/virtual-modules.ts +631 -0
- package/src/vite-env.d.ts +1 -0
|
@@ -0,0 +1,636 @@
|
|
|
1
|
+
// ponytail: talks to the `opencode serve` HTTP API directly — no SDK
|
|
2
|
+
// dependency. Endpoints on this fork (anomalyco): GET /session,
|
|
3
|
+
// GET /session/<id>/message (real per-session status lives here as message
|
|
4
|
+
// `parts[].state.status`), GET /event (SSE; /global/events is v1.x only).
|
|
5
|
+
// The old /session/status `attention` flag is still honored when present.
|
|
6
|
+
import type { Agent, AgentProvider, AgentStatus } from "../shared/state.js"
|
|
7
|
+
import {
|
|
8
|
+
opencodeEventToStatus,
|
|
9
|
+
type OpencodeEvent,
|
|
10
|
+
type OpencodeSessionStatus,
|
|
11
|
+
} from "../shared/opencode-status.js"
|
|
12
|
+
import {
|
|
13
|
+
readOpenCodeInstances,
|
|
14
|
+
type OpenCodeInstance,
|
|
15
|
+
} from "./opencode-instances.js"
|
|
16
|
+
|
|
17
|
+
// ponytail: "live" window — only sessions updated within this are surfaced.
|
|
18
|
+
const RECENT_WINDOW_MS = 6 * 60 * 60 * 1000
|
|
19
|
+
|
|
20
|
+
// ponytail: how many trailing messages to fetch per session for status. A
|
|
21
|
+
// mid-turn session's newest message streams with finish==null and empty parts,
|
|
22
|
+
// so the previous message(s) carry the live tool state — 6 is enough to see
|
|
23
|
+
// the open tool while the response streams.
|
|
24
|
+
const STATUS_MESSAGE_LIMIT = 6
|
|
25
|
+
|
|
26
|
+
interface OpencodeSessionLike {
|
|
27
|
+
readonly id: string
|
|
28
|
+
readonly title?: string
|
|
29
|
+
readonly directory?: string
|
|
30
|
+
readonly cost?: number
|
|
31
|
+
readonly contextTokens?: number
|
|
32
|
+
readonly contextPercent?: number
|
|
33
|
+
readonly agent?: string
|
|
34
|
+
readonly model?: { readonly id?: string; readonly providerID?: string }
|
|
35
|
+
readonly tokens?: TokenUsage
|
|
36
|
+
readonly time?: { readonly updated?: number; readonly created?: number }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type SessionStatusEntry = OpencodeSessionStatus & { attention?: boolean }
|
|
40
|
+
|
|
41
|
+
interface MessagePart {
|
|
42
|
+
readonly type?: string
|
|
43
|
+
readonly id?: string
|
|
44
|
+
readonly callID?: string
|
|
45
|
+
readonly tool_use_id?: string
|
|
46
|
+
readonly tool?: string
|
|
47
|
+
readonly state?: { readonly status?: string; readonly title?: string }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface TokenUsage {
|
|
51
|
+
readonly input?: number
|
|
52
|
+
readonly output?: number
|
|
53
|
+
readonly reasoning?: number
|
|
54
|
+
readonly cache?: { readonly read?: number; readonly write?: number }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface ProviderModelLike {
|
|
58
|
+
readonly limit?: { readonly context?: number }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface ProviderLike {
|
|
62
|
+
readonly id?: string
|
|
63
|
+
readonly models?: Readonly<Record<string, ProviderModelLike>>
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface MessageLike {
|
|
67
|
+
readonly info?: {
|
|
68
|
+
readonly role?: string
|
|
69
|
+
readonly tokens?: TokenUsage
|
|
70
|
+
readonly cost?: number
|
|
71
|
+
readonly finish?: string | null
|
|
72
|
+
readonly modelID?: string
|
|
73
|
+
readonly providerID?: string
|
|
74
|
+
}
|
|
75
|
+
readonly finish?: string | null
|
|
76
|
+
readonly cost?: number
|
|
77
|
+
readonly parts?: readonly MessagePart[]
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface OpencodeHttpApi {
|
|
81
|
+
listSessions(signal: AbortSignal): Promise<ReadonlyArray<OpencodeSessionLike>>
|
|
82
|
+
sessionStatus(
|
|
83
|
+
signal: AbortSignal,
|
|
84
|
+
): Promise<Record<string, SessionStatusEntry>>
|
|
85
|
+
sessionMessages(
|
|
86
|
+
signal: AbortSignal,
|
|
87
|
+
id: string,
|
|
88
|
+
limit: number,
|
|
89
|
+
): Promise<ReadonlyArray<MessageLike>>
|
|
90
|
+
providerModels?: (signal: AbortSignal) => Promise<readonly ProviderLike[]>
|
|
91
|
+
eventStream(
|
|
92
|
+
signal: AbortSignal,
|
|
93
|
+
): Promise<AsyncIterable<OpencodeEvent | unknown>>
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export const OPENCODE_LOGO =
|
|
97
|
+
"addon://coding-agents/assets/opencode-dark-square.svg"
|
|
98
|
+
|
|
99
|
+
// ponytail: every request gets a hard timeout — the opencode serve process
|
|
100
|
+
// can wedge (accepting connections, never answering) and an un-timed fetch
|
|
101
|
+
// would freeze the poll loop forever (observed live: daemon count froze for
|
|
102
|
+
// 25+ minutes with zero poll completions).
|
|
103
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 5_000
|
|
104
|
+
|
|
105
|
+
const requestSignal = (signal: AbortSignal, timeoutMs: number): AbortSignal =>
|
|
106
|
+
AbortSignal.any([signal, AbortSignal.timeout(timeoutMs)])
|
|
107
|
+
|
|
108
|
+
// Bounds any api call (including injected test doubles) so a wedged server
|
|
109
|
+
// can never leave a poll pending forever. The default httpApi additionally
|
|
110
|
+
// aborts the underlying fetch via requestSignal.
|
|
111
|
+
const withTimeout = <T>(p: Promise<T>, timeoutMs: number): Promise<T> => {
|
|
112
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
113
|
+
return Promise.race([
|
|
114
|
+
p,
|
|
115
|
+
new Promise<never>((_, reject) => {
|
|
116
|
+
timer = setTimeout(
|
|
117
|
+
() => reject(new Error("opencode request timed out")),
|
|
118
|
+
timeoutMs,
|
|
119
|
+
)
|
|
120
|
+
}),
|
|
121
|
+
]).finally(() => {
|
|
122
|
+
if (timer !== undefined) clearTimeout(timer)
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const requestJson = async <T>(
|
|
127
|
+
baseUrl: string,
|
|
128
|
+
path: string,
|
|
129
|
+
signal: AbortSignal,
|
|
130
|
+
timeoutMs: number,
|
|
131
|
+
): Promise<T> => {
|
|
132
|
+
const res = await fetch(`${baseUrl}${path}`, {
|
|
133
|
+
signal: requestSignal(signal, timeoutMs),
|
|
134
|
+
})
|
|
135
|
+
if (!res.ok) {
|
|
136
|
+
throw new Error(`opencode ${path} → HTTP ${res.status}`)
|
|
137
|
+
}
|
|
138
|
+
return (await res.json()) as T
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const httpApi = (baseUrl: string, timeoutMs: number): OpencodeHttpApi => ({
|
|
142
|
+
listSessions: (signal) => requestJson(baseUrl, "/session", signal, timeoutMs),
|
|
143
|
+
sessionStatus: (signal) =>
|
|
144
|
+
requestJson<Record<string, SessionStatusEntry>>(
|
|
145
|
+
baseUrl,
|
|
146
|
+
"/session/status",
|
|
147
|
+
signal,
|
|
148
|
+
timeoutMs,
|
|
149
|
+
),
|
|
150
|
+
sessionMessages: (signal, id, limit) =>
|
|
151
|
+
requestJson<ReadonlyArray<MessageLike>>(
|
|
152
|
+
baseUrl,
|
|
153
|
+
`/session/${encodeURIComponent(id)}/message?limit=${limit}`,
|
|
154
|
+
signal,
|
|
155
|
+
timeoutMs,
|
|
156
|
+
),
|
|
157
|
+
providerModels: async (signal) => {
|
|
158
|
+
const response = await requestJson<{ all?: readonly ProviderLike[] }>(
|
|
159
|
+
baseUrl,
|
|
160
|
+
"/provider",
|
|
161
|
+
signal,
|
|
162
|
+
timeoutMs,
|
|
163
|
+
)
|
|
164
|
+
return response.all ?? []
|
|
165
|
+
},
|
|
166
|
+
eventStream: async (signal) => streamEvents(baseUrl, signal, timeoutMs),
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
// ponytail: minimal SSE reader over fetch's streaming body. opencode
|
|
170
|
+
// emits `data: <json>\n\n` frames; we only need the data payloads.
|
|
171
|
+
async function* streamEvents(
|
|
172
|
+
baseUrl: string,
|
|
173
|
+
signal: AbortSignal,
|
|
174
|
+
timeoutMs: number,
|
|
175
|
+
): AsyncIterable<OpencodeEvent | unknown> {
|
|
176
|
+
// ponytail: the anomalyco fork serves the event stream at /event;
|
|
177
|
+
// v1.x uses /global/events. Either path can 200 with an HTML fallback, so
|
|
178
|
+
// validate the first chunk is a `data:` frame before committing.
|
|
179
|
+
for (const path of ["/event", "/global/events"] as const) {
|
|
180
|
+
try {
|
|
181
|
+
const attempt = await fetch(`${baseUrl}${path}`, {
|
|
182
|
+
signal: requestSignal(signal, timeoutMs),
|
|
183
|
+
headers: { Accept: "text/event-stream" },
|
|
184
|
+
})
|
|
185
|
+
if (!attempt.ok || attempt.body === null) continue
|
|
186
|
+
const reader = attempt.body.getReader()
|
|
187
|
+
const first = await reader.read()
|
|
188
|
+
reader.releaseLock()
|
|
189
|
+
if (first.done || !first.value || !startsDataFrame(first.value)) {
|
|
190
|
+
await attempt.body.cancel().catch(() => {})
|
|
191
|
+
continue
|
|
192
|
+
}
|
|
193
|
+
const res = attempt
|
|
194
|
+
void res
|
|
195
|
+
const decoder = new TextDecoder()
|
|
196
|
+
let buffer = new TextDecoder().decode(first.value as Uint8Array, {
|
|
197
|
+
stream: true,
|
|
198
|
+
})
|
|
199
|
+
const body = res.body as ReadableStream<Uint8Array>
|
|
200
|
+
for await (const chunk of body) {
|
|
201
|
+
buffer += decoder.decode(chunk as Uint8Array, { stream: true })
|
|
202
|
+
let idx: number
|
|
203
|
+
while ((idx = buffer.indexOf("\n\n")) !== -1) {
|
|
204
|
+
const frame = buffer.slice(0, idx)
|
|
205
|
+
buffer = buffer.slice(idx + 2)
|
|
206
|
+
for (const line of frame.split("\n")) {
|
|
207
|
+
if (!line.startsWith("data:")) continue
|
|
208
|
+
const payload = line.slice(5).trim()
|
|
209
|
+
if (payload.length === 0) continue
|
|
210
|
+
try {
|
|
211
|
+
yield JSON.parse(payload) as OpencodeEvent
|
|
212
|
+
} catch {
|
|
213
|
+
// non-JSON frame — ignore
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return
|
|
219
|
+
} catch {
|
|
220
|
+
continue
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const startsDataFrame = (chunk: Uint8Array): boolean =>
|
|
226
|
+
String.fromCharCode(...chunk.slice(0, 32)).includes("data:")
|
|
227
|
+
|
|
228
|
+
// ponytail: this fork's /session/status is empty even mid-turn, so live
|
|
229
|
+
// status comes from the trailing message parts. A `tool` part with
|
|
230
|
+
// state.status "pending" means the agent is waiting on the user (permission
|
|
231
|
+
// prompt or a `question`) → waiting_for_human; "running" → running. The
|
|
232
|
+
// newest message of an in-flight turn streams with finish==null and (often)
|
|
233
|
+
// empty parts — that's still "running". Otherwise the turn has finished.
|
|
234
|
+
const statusFromMessages = (
|
|
235
|
+
messages: ReadonlyArray<MessageLike>,
|
|
236
|
+
): AgentStatus | null => {
|
|
237
|
+
const completedTools = new Set<string>()
|
|
238
|
+
for (const message of messages) {
|
|
239
|
+
for (const part of message.parts ?? []) {
|
|
240
|
+
if (part.type !== "tool_result") continue
|
|
241
|
+
const id = part.id ?? part.callID ?? part.tool_use_id
|
|
242
|
+
if (id !== undefined) completedTools.add(id)
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
247
|
+
const m = messages[i]
|
|
248
|
+
const parts = m?.parts ?? []
|
|
249
|
+
for (const p of parts) {
|
|
250
|
+
if (p?.type !== "tool") continue
|
|
251
|
+
const id = p.id ?? p.callID ?? p.tool_use_id
|
|
252
|
+
if (id !== undefined && completedTools.has(id)) continue
|
|
253
|
+
const st = p.state?.status
|
|
254
|
+
if (st === "pending") return "waiting_for_human"
|
|
255
|
+
if (st === "running") return "running"
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
const last = messages[messages.length - 1]
|
|
259
|
+
if (last === undefined) return null
|
|
260
|
+
// ponytail: an in-flight turn streams with finish==null and empty parts —
|
|
261
|
+
// that's still "running". A finished turn carries a real finish value.
|
|
262
|
+
if (last.finish === null || last.info?.finish === null) return "running"
|
|
263
|
+
return "idle"
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const tokenTotal = (tokens: TokenUsage | undefined): number | undefined => {
|
|
267
|
+
if (tokens === undefined) return undefined
|
|
268
|
+
const values = [
|
|
269
|
+
tokens.input,
|
|
270
|
+
tokens.output,
|
|
271
|
+
tokens.reasoning,
|
|
272
|
+
tokens.cache?.read,
|
|
273
|
+
tokens.cache?.write,
|
|
274
|
+
]
|
|
275
|
+
let total = 0
|
|
276
|
+
for (const value of values) {
|
|
277
|
+
if (typeof value === "number" && Number.isFinite(value)) total += value
|
|
278
|
+
}
|
|
279
|
+
return total > 0 ? total : undefined
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const latestAssistantTokens = (
|
|
283
|
+
messages: ReadonlyArray<MessageLike>,
|
|
284
|
+
): number | undefined => {
|
|
285
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
286
|
+
const info = messages[i]?.info
|
|
287
|
+
if (info?.role !== "assistant" || (info.tokens?.output ?? 0) <= 0) {
|
|
288
|
+
continue
|
|
289
|
+
}
|
|
290
|
+
return tokenTotal(info.tokens)
|
|
291
|
+
}
|
|
292
|
+
return undefined
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const messageCost = (messages: ReadonlyArray<MessageLike>): number =>
|
|
296
|
+
messages.reduce((sum, message) => {
|
|
297
|
+
const cost = message.info?.cost ?? message.cost
|
|
298
|
+
return sum + (typeof cost === "number" && Number.isFinite(cost) ? cost : 0)
|
|
299
|
+
}, 0)
|
|
300
|
+
|
|
301
|
+
// ponytail: the /session list omits sessions owned by other opencode
|
|
302
|
+
// processes (verified live), but /session/<id>/message still answers. The
|
|
303
|
+
// newest assistant message carries modelID/providerID, enough to resolve a
|
|
304
|
+
// context limit and derive a percent for instance-only agents.
|
|
305
|
+
const latestMessageModel = (
|
|
306
|
+
messages: ReadonlyArray<MessageLike>,
|
|
307
|
+
): { providerID: string; modelID: string } | null => {
|
|
308
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
309
|
+
const info = messages[i]?.info
|
|
310
|
+
const providerID = info?.providerID
|
|
311
|
+
const modelID = info?.modelID
|
|
312
|
+
if (
|
|
313
|
+
typeof providerID === "string" &&
|
|
314
|
+
providerID.length > 0 &&
|
|
315
|
+
typeof modelID === "string" &&
|
|
316
|
+
modelID.length > 0
|
|
317
|
+
) {
|
|
318
|
+
return { providerID, modelID }
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return null
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ponytail: the fork's /session/status is usually empty, so live status comes
|
|
325
|
+
// from the message parts; when a message-derived status is idle, fall back to
|
|
326
|
+
// whatever the status map reported (map status is authoritative when present).
|
|
327
|
+
const combineStatus = (
|
|
328
|
+
fromMap: AgentStatus,
|
|
329
|
+
fromMessages: AgentStatus | null,
|
|
330
|
+
): AgentStatus => fromMessages ?? fromMap
|
|
331
|
+
|
|
332
|
+
export class OpenCodeProvider implements AgentProvider {
|
|
333
|
+
readonly id = "opencode" as const
|
|
334
|
+
readonly displayName = "OpenCode"
|
|
335
|
+
readonly logoPath = OPENCODE_LOGO
|
|
336
|
+
|
|
337
|
+
readonly #api: OpencodeHttpApi
|
|
338
|
+
readonly #apiFactory: (baseUrl: string, timeoutMs: number) => OpencodeHttpApi
|
|
339
|
+
readonly #recentWindowMs: number
|
|
340
|
+
readonly #requestTimeoutMs: number
|
|
341
|
+
|
|
342
|
+
constructor(opts: {
|
|
343
|
+
baseUrl: string
|
|
344
|
+
apiFactory?: (baseUrl: string, timeoutMs: number) => OpencodeHttpApi
|
|
345
|
+
recentWindowMs?: number
|
|
346
|
+
requestTimeoutMs?: number
|
|
347
|
+
}) {
|
|
348
|
+
this.#requestTimeoutMs = opts.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS
|
|
349
|
+
this.#apiFactory = opts.apiFactory ?? httpApi
|
|
350
|
+
this.#api = this.#apiFactory(opts.baseUrl, this.#requestTimeoutMs)
|
|
351
|
+
this.#recentWindowMs = opts.recentWindowMs ?? RECENT_WINDOW_MS
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async fetchSnapshot(signal: AbortSignal): Promise<readonly Agent[]> {
|
|
355
|
+
if (signal.aborted) return []
|
|
356
|
+
const instances = await readOpenCodeInstances()
|
|
357
|
+
try {
|
|
358
|
+
const [sessions, statusMap, providers] = await Promise.all([
|
|
359
|
+
withTimeout(this.#api.listSessions(signal), this.#requestTimeoutMs),
|
|
360
|
+
withTimeout(this.#api.sessionStatus(signal), this.#requestTimeoutMs),
|
|
361
|
+
withTimeout(
|
|
362
|
+
this.#api.providerModels?.(signal).catch(() => []) ??
|
|
363
|
+
Promise.resolve([]),
|
|
364
|
+
this.#requestTimeoutMs,
|
|
365
|
+
).catch(() => [] as readonly ProviderLike[]),
|
|
366
|
+
])
|
|
367
|
+
const contextLimits = new Map<string, number>()
|
|
368
|
+
for (const provider of providers) {
|
|
369
|
+
for (const [modelId, model] of Object.entries(provider.models ?? {})) {
|
|
370
|
+
const context = model.limit?.context
|
|
371
|
+
if (provider.id !== undefined && typeof context === "number") {
|
|
372
|
+
contextLimits.set(`${provider.id}:${modelId}`, context)
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
const now = Date.now()
|
|
377
|
+
const recent = sessions
|
|
378
|
+
.filter((s) => (s.time?.updated ?? 0) >= now - this.#recentWindowMs)
|
|
379
|
+
.sort((a, b) => (b.time?.updated ?? 0) - (a.time?.updated ?? 0))
|
|
380
|
+
const agents = await this.#withMessages(
|
|
381
|
+
recent,
|
|
382
|
+
statusMap,
|
|
383
|
+
signal,
|
|
384
|
+
contextLimits,
|
|
385
|
+
)
|
|
386
|
+
const bySessionId = new Map(
|
|
387
|
+
instances.flatMap((instance) =>
|
|
388
|
+
instance.sessionId === undefined
|
|
389
|
+
? []
|
|
390
|
+
: [[instance.sessionId, instance] as const],
|
|
391
|
+
),
|
|
392
|
+
)
|
|
393
|
+
const represented = new Set<string>()
|
|
394
|
+
const merged = agents.map((session) => {
|
|
395
|
+
const instance = bySessionId.get(session.sessionId)
|
|
396
|
+
if (instance === undefined) return session
|
|
397
|
+
represented.add(instance.instanceId)
|
|
398
|
+
return {
|
|
399
|
+
...session,
|
|
400
|
+
instanceId: instance.instanceId,
|
|
401
|
+
pid: instance.pid,
|
|
402
|
+
status: instance.status,
|
|
403
|
+
directory: instance.cwd,
|
|
404
|
+
updatedAt: instance.updatedAt,
|
|
405
|
+
}
|
|
406
|
+
})
|
|
407
|
+
return [
|
|
408
|
+
...merged,
|
|
409
|
+
...(await this.#instanceAgents(
|
|
410
|
+
instances.filter((instance) => !represented.has(instance.instanceId)),
|
|
411
|
+
contextLimits,
|
|
412
|
+
signal,
|
|
413
|
+
)),
|
|
414
|
+
]
|
|
415
|
+
} catch (err) {
|
|
416
|
+
if (signal.aborted) return []
|
|
417
|
+
console.warn(
|
|
418
|
+
"[coding-agents] opencode snapshot failed:",
|
|
419
|
+
err instanceof Error ? err.message : String(err),
|
|
420
|
+
)
|
|
421
|
+
return instances.map(toInstanceAgent)
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// ponytail: instances whose session is missing from the /session list get
|
|
426
|
+
// their metrics (tokens, cost, context %) derived from the message
|
|
427
|
+
// endpoint, which answers even when the list omits the session.
|
|
428
|
+
async #instanceAgents(
|
|
429
|
+
instances: ReadonlyArray<OpenCodeInstance>,
|
|
430
|
+
contextLimits: ReadonlyMap<string, number>,
|
|
431
|
+
signal: AbortSignal,
|
|
432
|
+
): Promise<readonly Agent[]> {
|
|
433
|
+
const out: Agent[] = []
|
|
434
|
+
for (const instance of instances) {
|
|
435
|
+
if (instance.sessionId === undefined || signal.aborted) {
|
|
436
|
+
out.push(toInstanceAgent(instance))
|
|
437
|
+
continue
|
|
438
|
+
}
|
|
439
|
+
try {
|
|
440
|
+
const messages = await withTimeout(
|
|
441
|
+
this.#api.sessionMessages(
|
|
442
|
+
signal,
|
|
443
|
+
instance.sessionId,
|
|
444
|
+
STATUS_MESSAGE_LIMIT,
|
|
445
|
+
),
|
|
446
|
+
this.#requestTimeoutMs,
|
|
447
|
+
)
|
|
448
|
+
const contextTokens = latestAssistantTokens(messages)
|
|
449
|
+
let cost: number | undefined
|
|
450
|
+
const derivedCost = messageCost(messages)
|
|
451
|
+
if (derivedCost > 0) cost = derivedCost
|
|
452
|
+
let contextPercent: number | undefined
|
|
453
|
+
const model = latestMessageModel(messages)
|
|
454
|
+
const limit =
|
|
455
|
+
model !== null
|
|
456
|
+
? contextLimits.get(`${model.providerID}:${model.modelID}`)
|
|
457
|
+
: undefined
|
|
458
|
+
if (contextTokens !== undefined && limit !== undefined) {
|
|
459
|
+
contextPercent = Math.round((contextTokens / limit) * 100)
|
|
460
|
+
}
|
|
461
|
+
out.push({
|
|
462
|
+
...toInstanceAgent(instance),
|
|
463
|
+
...(cost !== undefined ? { cost } : {}),
|
|
464
|
+
...(contextTokens !== undefined ? { contextTokens } : {}),
|
|
465
|
+
...(contextPercent !== undefined ? { contextPercent } : {}),
|
|
466
|
+
})
|
|
467
|
+
} catch {
|
|
468
|
+
out.push(toInstanceAgent(instance))
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return out
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
async #withMessages(
|
|
475
|
+
recent: ReadonlyArray<OpencodeSessionLike>,
|
|
476
|
+
statusMap: Record<string, SessionStatusEntry>,
|
|
477
|
+
signal: AbortSignal,
|
|
478
|
+
contextLimits: ReadonlyMap<string, number>,
|
|
479
|
+
): Promise<readonly Agent[]> {
|
|
480
|
+
const out: Agent[] = []
|
|
481
|
+
for (const s of recent) {
|
|
482
|
+
const fromMap = toStatusFromMap(s, statusMap)
|
|
483
|
+
let status = fromMap
|
|
484
|
+
let contextTokens = tokenTotal(s.tokens)
|
|
485
|
+
let cost =
|
|
486
|
+
typeof s.cost === "number" && Number.isFinite(s.cost)
|
|
487
|
+
? s.cost
|
|
488
|
+
: undefined
|
|
489
|
+
if (status !== "waiting_for_human") {
|
|
490
|
+
try {
|
|
491
|
+
const messages = await withTimeout(
|
|
492
|
+
this.#api.sessionMessages(signal, s.id, STATUS_MESSAGE_LIMIT),
|
|
493
|
+
this.#requestTimeoutMs,
|
|
494
|
+
)
|
|
495
|
+
status = combineStatus(fromMap, statusFromMessages(messages))
|
|
496
|
+
contextTokens = latestAssistantTokens(messages) ?? contextTokens
|
|
497
|
+
if (cost === undefined) {
|
|
498
|
+
const derivedCost = messageCost(messages)
|
|
499
|
+
if (derivedCost > 0) cost = derivedCost
|
|
500
|
+
}
|
|
501
|
+
} catch {
|
|
502
|
+
// keep derived status; message endpoint may 4xx for closed sessions
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
const contextLimit =
|
|
506
|
+
s.model?.providerID !== undefined && s.model.id !== undefined
|
|
507
|
+
? contextLimits.get(`${s.model.providerID}:${s.model.id}`)
|
|
508
|
+
: undefined
|
|
509
|
+
const contextPercent =
|
|
510
|
+
contextTokens !== undefined && contextLimit !== undefined
|
|
511
|
+
? Math.round((contextTokens / contextLimit) * 100)
|
|
512
|
+
: undefined
|
|
513
|
+
out.push(
|
|
514
|
+
toAgent(s, status, {
|
|
515
|
+
...(s.directory !== undefined ? { directory: s.directory } : {}),
|
|
516
|
+
...(cost !== undefined ? { cost } : {}),
|
|
517
|
+
...(contextTokens !== undefined ? { contextTokens } : {}),
|
|
518
|
+
...(contextPercent !== undefined ? { contextPercent } : {}),
|
|
519
|
+
}),
|
|
520
|
+
)
|
|
521
|
+
}
|
|
522
|
+
return out
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
subscribe(signal: AbortSignal, onChange: () => void): () => void {
|
|
526
|
+
let cancelled = false
|
|
527
|
+
const setCancelled = (): void => {
|
|
528
|
+
cancelled = true
|
|
529
|
+
}
|
|
530
|
+
signal.addEventListener("abort", setCancelled)
|
|
531
|
+
const noop = (): void => {}
|
|
532
|
+
|
|
533
|
+
const run = async () => {
|
|
534
|
+
outer: while (!cancelled && !signal.aborted) {
|
|
535
|
+
try {
|
|
536
|
+
const sub = await this.#api.eventStream(signal)
|
|
537
|
+
try {
|
|
538
|
+
for await (const raw of sub) {
|
|
539
|
+
if (cancelled || signal.aborted) break
|
|
540
|
+
const evt = raw as OpencodeEvent
|
|
541
|
+
if (!evt || typeof evt !== "object") continue
|
|
542
|
+
if (!("type" in evt)) continue
|
|
543
|
+
const mapped = opencodeEventToStatus(evt)
|
|
544
|
+
if (mapped) {
|
|
545
|
+
onChange()
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
} finally {
|
|
549
|
+
noop()
|
|
550
|
+
}
|
|
551
|
+
if (cancelled || signal.aborted) break outer
|
|
552
|
+
await new Promise<void>((resolve) => {
|
|
553
|
+
if (signal.aborted) {
|
|
554
|
+
resolve()
|
|
555
|
+
return
|
|
556
|
+
}
|
|
557
|
+
const t = setTimeout(resolve, 50)
|
|
558
|
+
signal.addEventListener("abort", () => {
|
|
559
|
+
clearTimeout(t)
|
|
560
|
+
resolve()
|
|
561
|
+
})
|
|
562
|
+
})
|
|
563
|
+
} catch {
|
|
564
|
+
await new Promise((resolve) => {
|
|
565
|
+
const t = setTimeout(resolve, 3000)
|
|
566
|
+
signal.addEventListener("abort", () => {
|
|
567
|
+
clearTimeout(t)
|
|
568
|
+
resolve(undefined)
|
|
569
|
+
})
|
|
570
|
+
})
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
void run()
|
|
576
|
+
return () => {
|
|
577
|
+
cancelled = true
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const toStatusFromMap = (
|
|
583
|
+
s: OpencodeSessionLike,
|
|
584
|
+
statusMap: Record<string, SessionStatusEntry>,
|
|
585
|
+
): AgentStatus => {
|
|
586
|
+
const status = statusMap[s.id]
|
|
587
|
+
let normalizedStatus: AgentStatus = "idle"
|
|
588
|
+
if (status?.type === "busy") normalizedStatus = "running"
|
|
589
|
+
else if (status?.type === "retry") normalizedStatus = "waiting"
|
|
590
|
+
// ponytail: opencode v1.1+ surfaces an `attention` boolean on
|
|
591
|
+
// /session/status entries when the assistant needs human input — trust
|
|
592
|
+
// it over our inference since it comes from the source itself.
|
|
593
|
+
if (status?.attention === true) normalizedStatus = "waiting_for_human"
|
|
594
|
+
return normalizedStatus
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const toAgent = (
|
|
598
|
+
s: OpencodeSessionLike,
|
|
599
|
+
status: AgentStatus,
|
|
600
|
+
extra: {
|
|
601
|
+
directory?: string
|
|
602
|
+
cost?: number
|
|
603
|
+
contextTokens?: number
|
|
604
|
+
contextPercent?: number
|
|
605
|
+
},
|
|
606
|
+
): Agent => ({
|
|
607
|
+
sessionId: s.id,
|
|
608
|
+
providerId: "opencode",
|
|
609
|
+
title: s.title && s.title.length > 0 ? s.title : "Untitled session",
|
|
610
|
+
status,
|
|
611
|
+
...(extra.directory !== undefined ? { directory: extra.directory } : {}),
|
|
612
|
+
...(extra.cost !== undefined && Number.isFinite(extra.cost)
|
|
613
|
+
? { cost: extra.cost }
|
|
614
|
+
: {}),
|
|
615
|
+
...(extra.contextTokens !== undefined && Number.isFinite(extra.contextTokens)
|
|
616
|
+
? { contextTokens: extra.contextTokens }
|
|
617
|
+
: {}),
|
|
618
|
+
...(extra.contextPercent !== undefined &&
|
|
619
|
+
Number.isFinite(extra.contextPercent)
|
|
620
|
+
? { contextPercent: extra.contextPercent }
|
|
621
|
+
: {}),
|
|
622
|
+
...(s.time?.created !== undefined ? { createdAt: s.time.created } : {}),
|
|
623
|
+
updatedAt: s.time?.updated ?? s.time?.created ?? 0,
|
|
624
|
+
})
|
|
625
|
+
|
|
626
|
+
const toInstanceAgent = (instance: OpenCodeInstance): Agent => ({
|
|
627
|
+
sessionId: instance.sessionId ?? `instance:${instance.instanceId}`,
|
|
628
|
+
instanceId: instance.instanceId,
|
|
629
|
+
pid: instance.pid,
|
|
630
|
+
providerId: "opencode",
|
|
631
|
+
title: "OpenCode",
|
|
632
|
+
status: instance.status,
|
|
633
|
+
directory: instance.cwd,
|
|
634
|
+
updatedAt: instance.updatedAt,
|
|
635
|
+
createdAt: instance.updatedAt,
|
|
636
|
+
})
|