@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,470 @@
|
|
|
1
|
+
import { open, readdir, stat } from "node:fs/promises"
|
|
2
|
+
import { basename, join } from "node:path"
|
|
3
|
+
|
|
4
|
+
import chokidar, { type FSWatcher } from "chokidar"
|
|
5
|
+
|
|
6
|
+
// ponytail: see packages/addons/app-shortcuts/src/index.ts for context.
|
|
7
|
+
import type { Agent, AgentProvider } from "../shared/state.js"
|
|
8
|
+
import {
|
|
9
|
+
deriveClaudeStatus,
|
|
10
|
+
type ClaudeJsonlEntry,
|
|
11
|
+
} from "../shared/claude-status.js"
|
|
12
|
+
import { readClaudeInstances, type ClaudeInstance } from "./claude-instances.js"
|
|
13
|
+
|
|
14
|
+
export const CLAUDE_LOGO = "addon://coding-agents/assets/claude-code.svg"
|
|
15
|
+
|
|
16
|
+
export interface ClaudeCodeProviderOptions {
|
|
17
|
+
readonly projectsDir?: string
|
|
18
|
+
readonly recentWindowMs?: number
|
|
19
|
+
/** Off in tests that only exercise transcript ingest. */
|
|
20
|
+
readonly readLeases?: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const DEFAULT_PROJECTS_DIR = join(
|
|
24
|
+
process.env["HOME"] ?? "~",
|
|
25
|
+
".claude",
|
|
26
|
+
"projects",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
// ponytail: "live" window — sessions idle longer than this stay off the deck.
|
|
30
|
+
const RECENT_WINDOW_MS = 6 * 60 * 60 * 1000
|
|
31
|
+
|
|
32
|
+
// ponytail: the reporting machine had 136 MB across 105 transcripts, and the
|
|
33
|
+
// old ingest read EVERY file in full on startup and re-read a whole multi-MB
|
|
34
|
+
// file on every append. Enumerate by mtime, read only the tail, and cap the
|
|
35
|
+
// working set.
|
|
36
|
+
const MAX_SESSIONS = 32
|
|
37
|
+
const TAIL_BYTES = 256 * 1024
|
|
38
|
+
const TAIL_RETRY_BYTES = 1024 * 1024
|
|
39
|
+
const SCAN_DEBOUNCE_MS = 1_500
|
|
40
|
+
|
|
41
|
+
// A session transcript is named for its UUID. Subagent transcripts live in a
|
|
42
|
+
// `subagents/` subdirectory as `agent-<hex>.jsonl`, and abandoned ones are
|
|
43
|
+
// renamed `<uuid>.orphaned-<ts>-<hash>.jsonl`. This one pattern excludes both:
|
|
44
|
+
// 56 of those 105 files were subagents, and each would otherwise have been
|
|
45
|
+
// surfaced as its own agent tile — exactly the harness noise the summary is
|
|
46
|
+
// meant to aggregate away.
|
|
47
|
+
const SESSION_FILE_RE =
|
|
48
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.jsonl$/i
|
|
49
|
+
|
|
50
|
+
interface TailState {
|
|
51
|
+
size: number
|
|
52
|
+
entries: ClaudeJsonlEntry[]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export class ClaudeCodeProvider implements AgentProvider {
|
|
56
|
+
readonly id = "claude-code" as const
|
|
57
|
+
readonly displayName = "Claude Code"
|
|
58
|
+
readonly logoPath = CLAUDE_LOGO
|
|
59
|
+
|
|
60
|
+
readonly #projectsDir: string
|
|
61
|
+
readonly #recentWindowMs: number
|
|
62
|
+
readonly #leasesEnabled: boolean
|
|
63
|
+
|
|
64
|
+
constructor(opts: ClaudeCodeProviderOptions = {}) {
|
|
65
|
+
this.#projectsDir = opts.projectsDir ?? DEFAULT_PROJECTS_DIR
|
|
66
|
+
this.#recentWindowMs = opts.recentWindowMs ?? RECENT_WINDOW_MS
|
|
67
|
+
this.#leasesEnabled = opts.readLeases ?? true
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async fetchSnapshot(_signal: AbortSignal): Promise<readonly Agent[]> {
|
|
71
|
+
// ponytail: this used to serve purely from a map that only subscribe()
|
|
72
|
+
// ever filled — and subscribe() was never called, because the host never
|
|
73
|
+
// wired `globalService.subscriptions`. One missing call silently produced
|
|
74
|
+
// "zero Claude Code sessions" forever. fetchSnapshot now scans on its own
|
|
75
|
+
// (debounced, since the poller runs every 2s) and subscribe() is only a
|
|
76
|
+
// latency optimisation, so neither can silently disable the other.
|
|
77
|
+
await this.#maybeScan()
|
|
78
|
+
const cutoff = Date.now() - this.#recentWindowMs
|
|
79
|
+
const fromTranscripts = [...this.#agents.values()].filter(
|
|
80
|
+
(a) => a.updatedAt >= cutoff,
|
|
81
|
+
)
|
|
82
|
+
const leases = this.#leasesEnabled
|
|
83
|
+
? await readClaudeInstances({ prune: true })
|
|
84
|
+
: []
|
|
85
|
+
return mergeLeases(fromTranscripts, leases)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
#lastScanAt = 0
|
|
89
|
+
#scanInFlight: Promise<void> | null = null
|
|
90
|
+
|
|
91
|
+
async #maybeScan(): Promise<void> {
|
|
92
|
+
if (this.#scanInFlight !== null) return this.#scanInFlight
|
|
93
|
+
if (Date.now() - this.#lastScanAt < SCAN_DEBOUNCE_MS) return
|
|
94
|
+
const run = this.#scan(() => undefined).finally(() => {
|
|
95
|
+
this.#lastScanAt = Date.now()
|
|
96
|
+
this.#scanInFlight = null
|
|
97
|
+
})
|
|
98
|
+
this.#scanInFlight = run
|
|
99
|
+
return run
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
subscribe(signal: AbortSignal, onChange: () => void): () => void {
|
|
103
|
+
const watcher: FSWatcher = chokidar.watch(this.#projectsDir, {
|
|
104
|
+
// ponytail: transcripts live at projects/<slug>/<uuid>.jsonl, so depth 2
|
|
105
|
+
// is enough. Depth 6 also indexed every subagents/, tool-results/ and
|
|
106
|
+
// memory/ directory — thousands of files chokidar had no use for.
|
|
107
|
+
ignored: (path) =>
|
|
108
|
+
path.endsWith(".lock") ||
|
|
109
|
+
path.includes("/subagents/") ||
|
|
110
|
+
path.includes("/tool-results/") ||
|
|
111
|
+
path.includes("/memory/"),
|
|
112
|
+
persistent: true,
|
|
113
|
+
ignoreInitial: false,
|
|
114
|
+
depth: 2,
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
let timer: NodeJS.Timeout | null = null
|
|
118
|
+
|
|
119
|
+
const schedule = (): void => {
|
|
120
|
+
// ponytail: this used to start a NEW timer per event, so a burst of N
|
|
121
|
+
// appends created N timers; only the first did any work and the rest
|
|
122
|
+
// still fired. Coalesce onto a single pending timer instead.
|
|
123
|
+
if (timer !== null) return
|
|
124
|
+
timer = setTimeout(() => {
|
|
125
|
+
timer = null
|
|
126
|
+
onChange()
|
|
127
|
+
}, 250)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
void this.#scan(onChange)
|
|
131
|
+
|
|
132
|
+
const touch = (filePath: string): void => {
|
|
133
|
+
if (!isSessionTranscript(filePath)) return
|
|
134
|
+
void this.#ingestFile(filePath, schedule)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
watcher.on("add", touch)
|
|
138
|
+
watcher.on("change", touch)
|
|
139
|
+
watcher.on("unlink", (filePath: string) => {
|
|
140
|
+
if (!isSessionTranscript(filePath)) return
|
|
141
|
+
this.#tails.delete(filePath)
|
|
142
|
+
if (this.#agents.delete(sessionIdFromPath(filePath))) schedule()
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
signal.addEventListener("abort", () => {
|
|
146
|
+
void watcher.close()
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
return () => {
|
|
150
|
+
if (timer !== null) clearTimeout(timer)
|
|
151
|
+
void watcher.close()
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
readonly #agents = new Map<string, Agent>()
|
|
156
|
+
|
|
157
|
+
async #scan(onChange: () => void): Promise<void> {
|
|
158
|
+
// ponytail: enumerate by NAME and MTIME only — never by opening files.
|
|
159
|
+
// On the reporting machine this takes the candidate set from 105 files
|
|
160
|
+
// (136 MB) to 9, before a single byte of transcript is read.
|
|
161
|
+
let found: Array<{ path: string; mtimeMs: number }> = []
|
|
162
|
+
try {
|
|
163
|
+
const entries = await readdir(this.#projectsDir, {
|
|
164
|
+
withFileTypes: true,
|
|
165
|
+
recursive: true,
|
|
166
|
+
})
|
|
167
|
+
const cutoff = Date.now() - this.#recentWindowMs
|
|
168
|
+
const stats = await Promise.all(
|
|
169
|
+
entries
|
|
170
|
+
.filter((e) => e.isFile() && isSessionTranscript(e.name))
|
|
171
|
+
.map(async (e) => {
|
|
172
|
+
const full = join(e.parentPath ?? this.#projectsDir, e.name)
|
|
173
|
+
// Belt and braces: a rename of the subagent layout must not start
|
|
174
|
+
// surfacing them as top-level agents.
|
|
175
|
+
if (full.includes("/subagents/")) return null
|
|
176
|
+
try {
|
|
177
|
+
const st = await stat(full)
|
|
178
|
+
return st.mtimeMs >= cutoff
|
|
179
|
+
? { path: full, mtimeMs: st.mtimeMs }
|
|
180
|
+
: null
|
|
181
|
+
} catch {
|
|
182
|
+
return null
|
|
183
|
+
}
|
|
184
|
+
}),
|
|
185
|
+
)
|
|
186
|
+
found = stats.filter(
|
|
187
|
+
(x): x is { path: string; mtimeMs: number } => x !== null,
|
|
188
|
+
)
|
|
189
|
+
} catch {
|
|
190
|
+
return
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
found.sort((a, b) => b.mtimeMs - a.mtimeMs)
|
|
194
|
+
const live = found.slice(0, MAX_SESSIONS)
|
|
195
|
+
const liveIds = new Set(live.map((f) => sessionIdFromPath(f.path)))
|
|
196
|
+
|
|
197
|
+
for (const file of live) {
|
|
198
|
+
await this.#ingestFile(file.path, onChange, file.mtimeMs)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Drop anything no longer live, and keep the tail cache bounded alongside.
|
|
202
|
+
for (const id of [...this.#agents.keys()]) {
|
|
203
|
+
if (!liveIds.has(id)) this.#agents.delete(id)
|
|
204
|
+
}
|
|
205
|
+
for (const path of [...this.#tails.keys()]) {
|
|
206
|
+
if (!live.some((f) => f.path === path)) this.#tails.delete(path)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
readonly #tails = new Map<string, TailState>()
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Reads only what is new. The first read takes the tail of the file; later
|
|
214
|
+
* reads take just the bytes appended since. A whole transcript is never read
|
|
215
|
+
* again — the largest here is 8 MB and grows while a session runs.
|
|
216
|
+
*/
|
|
217
|
+
async #readNewEntries(
|
|
218
|
+
filePath: string,
|
|
219
|
+
size: number,
|
|
220
|
+
): Promise<ClaudeJsonlEntry[] | null> {
|
|
221
|
+
const prev = this.#tails.get(filePath)
|
|
222
|
+
// Truncated or rotated → start over.
|
|
223
|
+
const from = prev !== undefined && size >= prev.size ? prev.size : null
|
|
224
|
+
|
|
225
|
+
const readFrom = async (offset: number): Promise<string | null> => {
|
|
226
|
+
const length = size - offset
|
|
227
|
+
if (length <= 0) return ""
|
|
228
|
+
const handle = await open(filePath, "r")
|
|
229
|
+
try {
|
|
230
|
+
const buf = Buffer.allocUnsafe(length)
|
|
231
|
+
await handle.read(buf, 0, length, offset)
|
|
232
|
+
return buf.toString("utf8")
|
|
233
|
+
} finally {
|
|
234
|
+
await handle.close()
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
try {
|
|
239
|
+
if (from !== null) {
|
|
240
|
+
const chunk = await readFrom(from)
|
|
241
|
+
if (chunk === null) return null
|
|
242
|
+
const appended = parseEntries(chunk)
|
|
243
|
+
const entries = [...prev!.entries, ...appended].slice(-MAX_ENTRIES)
|
|
244
|
+
this.#tails.set(filePath, { size, entries })
|
|
245
|
+
return entries
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// First sight of this file: take the tail, dropping the leading partial
|
|
249
|
+
// line. Retry once wider if the window caught no assistant entry — a
|
|
250
|
+
// single tool result can be larger than the default tail.
|
|
251
|
+
for (const window of [TAIL_BYTES, TAIL_RETRY_BYTES]) {
|
|
252
|
+
const offset = Math.max(0, size - window)
|
|
253
|
+
const chunk = await readFrom(offset)
|
|
254
|
+
if (chunk === null) return null
|
|
255
|
+
const body = offset > 0 ? chunk.slice(chunk.indexOf("\n") + 1) : chunk
|
|
256
|
+
const entries = parseEntries(body).slice(-MAX_ENTRIES)
|
|
257
|
+
const usable =
|
|
258
|
+
offset === 0 || entries.some((e) => e.type === "assistant")
|
|
259
|
+
if (usable || window === TAIL_RETRY_BYTES) {
|
|
260
|
+
this.#tails.set(filePath, { size, entries })
|
|
261
|
+
return entries
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return null
|
|
265
|
+
} catch {
|
|
266
|
+
return null
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async #ingestFile(
|
|
271
|
+
filePath: string,
|
|
272
|
+
onChange: () => void,
|
|
273
|
+
mtimeMs?: number,
|
|
274
|
+
): Promise<void> {
|
|
275
|
+
const sessionId = sessionIdFromPath(filePath)
|
|
276
|
+
try {
|
|
277
|
+
const st = await stat(filePath)
|
|
278
|
+
const entries = await this.#readNewEntries(filePath, st.size)
|
|
279
|
+
if (entries === null || entries.length === 0) return
|
|
280
|
+
|
|
281
|
+
// Pass mtime as the fallback so a transcript with no timestamps reports
|
|
282
|
+
// when the file last changed, rather than "now" — which made such files
|
|
283
|
+
// permanently live.
|
|
284
|
+
const derived = deriveClaudeStatus(
|
|
285
|
+
entries,
|
|
286
|
+
Date.now(),
|
|
287
|
+
mtimeMs ?? st.mtimeMs,
|
|
288
|
+
)
|
|
289
|
+
if (derived === null) return
|
|
290
|
+
|
|
291
|
+
const existing = this.#agents.get(sessionId)
|
|
292
|
+
const title = titleFromEntries(entries) ?? existing?.title ?? sessionId
|
|
293
|
+
const usage = contextTokensFromEntries(entries)
|
|
294
|
+
const agent: Agent = {
|
|
295
|
+
sessionId,
|
|
296
|
+
providerId: "claude-code",
|
|
297
|
+
title,
|
|
298
|
+
status: derived.status,
|
|
299
|
+
updatedAt: derived.updatedAt,
|
|
300
|
+
...(derived.createdAt !== undefined
|
|
301
|
+
? { createdAt: derived.createdAt }
|
|
302
|
+
: {}),
|
|
303
|
+
...(derived.preview !== undefined
|
|
304
|
+
? { lastMessagePreview: derived.preview }
|
|
305
|
+
: {}),
|
|
306
|
+
...(usage !== undefined ? { contextTokens: usage } : {}),
|
|
307
|
+
...(derived.cwd !== undefined
|
|
308
|
+
? { directory: derived.cwd }
|
|
309
|
+
: existing?.directory !== undefined
|
|
310
|
+
? { directory: existing.directory }
|
|
311
|
+
: {}),
|
|
312
|
+
}
|
|
313
|
+
this.#agents.set(sessionId, agent)
|
|
314
|
+
onChange()
|
|
315
|
+
} catch {
|
|
316
|
+
// file may be unreadable; skip
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const MAX_ENTRIES = 400
|
|
322
|
+
|
|
323
|
+
const parseEntries = (raw: string): ClaudeJsonlEntry[] => {
|
|
324
|
+
const out: ClaudeJsonlEntry[] = []
|
|
325
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
326
|
+
const trimmed = line.trim()
|
|
327
|
+
if (trimmed.length === 0) continue
|
|
328
|
+
try {
|
|
329
|
+
out.push(JSON.parse(trimmed) as ClaudeJsonlEntry)
|
|
330
|
+
} catch {
|
|
331
|
+
// skip malformed line
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return out
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Context size of the newest real assistant turn.
|
|
339
|
+
*
|
|
340
|
+
* ponytail: `costUSD` no longer exists in Claude Code 2.1.x transcripts, so the
|
|
341
|
+
* old cost sum was permanently zero. Real accounting lives on `message.usage`.
|
|
342
|
+
* Sidechain (subagent) entries are skipped so a subagent's context does not
|
|
343
|
+
* masquerade as the session's.
|
|
344
|
+
*/
|
|
345
|
+
const contextTokensFromEntries = (
|
|
346
|
+
entries: readonly ClaudeJsonlEntry[],
|
|
347
|
+
): number | undefined => {
|
|
348
|
+
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
349
|
+
const e = entries[i] as
|
|
350
|
+
| (ClaudeJsonlEntry & {
|
|
351
|
+
isSidechain?: boolean
|
|
352
|
+
message?: { usage?: Record<string, unknown> }
|
|
353
|
+
})
|
|
354
|
+
| undefined
|
|
355
|
+
if (e?.type !== "assistant" || e.isSidechain === true) continue
|
|
356
|
+
const usage = e.message?.usage
|
|
357
|
+
if (usage === undefined) continue
|
|
358
|
+
const num = (k: string): number =>
|
|
359
|
+
typeof usage[k] === "number" ? (usage[k] as number) : 0
|
|
360
|
+
if (num("output_tokens") <= 0) continue
|
|
361
|
+
const total =
|
|
362
|
+
num("input_tokens") +
|
|
363
|
+
num("cache_creation_input_tokens") +
|
|
364
|
+
num("cache_read_input_tokens") +
|
|
365
|
+
num("output_tokens")
|
|
366
|
+
return total > 0 ? total : undefined
|
|
367
|
+
}
|
|
368
|
+
return undefined
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* True only for a top-level session transcript.
|
|
373
|
+
*
|
|
374
|
+
* Accepts `<uuid>.jsonl` and nothing else, which excludes subagent transcripts
|
|
375
|
+
* (`subagents/agent-<hex>.jsonl`) and abandoned ones
|
|
376
|
+
* (`<uuid>.orphaned-<ts>-<hash>.jsonl`) in a single rule.
|
|
377
|
+
*/
|
|
378
|
+
const isSessionTranscript = (pathOrName: string): boolean => {
|
|
379
|
+
const base = pathOrName.split("/").pop() ?? pathOrName
|
|
380
|
+
return SESSION_FILE_RE.test(base)
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const sessionIdFromPath = (filePath: string): string => {
|
|
384
|
+
const base = filePath.split("/").pop() ?? filePath
|
|
385
|
+
return base.replace(/\.jsonl$/, "")
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const titleFromEntries = (
|
|
389
|
+
entries: readonly ClaudeJsonlEntry[],
|
|
390
|
+
): string | undefined => {
|
|
391
|
+
for (const e of entries) {
|
|
392
|
+
const summary = (e as { summary?: unknown }).summary
|
|
393
|
+
if (typeof summary === "string" && summary.length > 0) return summary
|
|
394
|
+
}
|
|
395
|
+
for (const e of entries) {
|
|
396
|
+
if (e.type === "user") {
|
|
397
|
+
const content = flattenUserText(e.message?.content)
|
|
398
|
+
if (content) return content
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return undefined
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const flattenUserText = (content: unknown): string | undefined => {
|
|
405
|
+
if (typeof content === "string") {
|
|
406
|
+
return content.length > 80 ? `${content.slice(0, 79)}…` : content
|
|
407
|
+
}
|
|
408
|
+
if (Array.isArray(content)) {
|
|
409
|
+
for (const part of content) {
|
|
410
|
+
if (
|
|
411
|
+
part !== null &&
|
|
412
|
+
typeof part === "object" &&
|
|
413
|
+
"text" in part &&
|
|
414
|
+
typeof (part as { text: unknown }).text === "string"
|
|
415
|
+
) {
|
|
416
|
+
const text = (part as { text: string }).text
|
|
417
|
+
return text.length > 80 ? `${text.slice(0, 79)}…` : text
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return undefined
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Folds hook-written leases over transcript-derived agents.
|
|
426
|
+
*
|
|
427
|
+
* ponytail: dedup MUST happen here, not in the frontend. The summary tile
|
|
428
|
+
* counts by flattening every provider's list, so emitting both a lease agent
|
|
429
|
+
* and a transcript agent for one session would double-count it.
|
|
430
|
+
*
|
|
431
|
+
* The lease wins anything it knows first-hand — status, directory, and the
|
|
432
|
+
* instance identity a transcript cannot supply. The transcript keeps what only
|
|
433
|
+
* it has: the title, the preview and the context size.
|
|
434
|
+
*/
|
|
435
|
+
export const mergeLeases = (
|
|
436
|
+
fromTranscripts: readonly Agent[],
|
|
437
|
+
leases: readonly ClaudeInstance[],
|
|
438
|
+
): readonly Agent[] => {
|
|
439
|
+
const bySession = new Map<string, Agent>()
|
|
440
|
+
for (const agent of fromTranscripts) bySession.set(agent.sessionId, agent)
|
|
441
|
+
|
|
442
|
+
for (const lease of leases) {
|
|
443
|
+
const existing = bySession.get(lease.sessionId)
|
|
444
|
+
if (existing === undefined) {
|
|
445
|
+
// A session whose transcript has not been flushed yet: surface it now
|
|
446
|
+
// rather than making the user wait for the first write.
|
|
447
|
+
bySession.set(lease.sessionId, {
|
|
448
|
+
sessionId: lease.sessionId,
|
|
449
|
+
instanceId: lease.instanceId,
|
|
450
|
+
providerId: "claude-code",
|
|
451
|
+
title: basename(lease.cwd) || "Claude Code",
|
|
452
|
+
status: lease.status,
|
|
453
|
+
directory: lease.cwd,
|
|
454
|
+
createdAt: lease.updatedAt,
|
|
455
|
+
updatedAt: lease.updatedAt,
|
|
456
|
+
...(lease.pid !== null ? { pid: lease.pid } : {}),
|
|
457
|
+
})
|
|
458
|
+
continue
|
|
459
|
+
}
|
|
460
|
+
bySession.set(lease.sessionId, {
|
|
461
|
+
...existing,
|
|
462
|
+
instanceId: lease.instanceId,
|
|
463
|
+
status: lease.status,
|
|
464
|
+
directory: lease.cwd,
|
|
465
|
+
updatedAt: Math.max(existing.updatedAt, lease.updatedAt),
|
|
466
|
+
...(lease.pid !== null ? { pid: lease.pid } : {}),
|
|
467
|
+
})
|
|
468
|
+
}
|
|
469
|
+
return [...bySession.values()]
|
|
470
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { readFile, readdir, stat, unlink } from "node:fs/promises"
|
|
2
|
+
import { homedir } from "node:os"
|
|
3
|
+
import { join } from "node:path"
|
|
4
|
+
|
|
5
|
+
import type { AgentStatus } from "../shared/state.js"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A live Claude Code session, as reported by the hook script.
|
|
9
|
+
*
|
|
10
|
+
* ponytail: the OpenCode equivalent comes from a plugin injected into
|
|
11
|
+
* OpenCode's own config. Claude Code needs no plugin — it has first-class
|
|
12
|
+
* hooks, which are the supported way to be told about session state. Same
|
|
13
|
+
* lease directory and the same defensive parsing, so the two sources behave
|
|
14
|
+
* identically from the provider's point of view.
|
|
15
|
+
*/
|
|
16
|
+
export interface ClaudeInstance {
|
|
17
|
+
readonly instanceId: string
|
|
18
|
+
readonly sessionId: string
|
|
19
|
+
readonly pid: number | null
|
|
20
|
+
readonly cwd: string
|
|
21
|
+
readonly status: AgentStatus
|
|
22
|
+
readonly updatedAt: number
|
|
23
|
+
readonly transcriptPath?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A `running`/`compacting` lease this old has missed its terminating event —
|
|
28
|
+
* a hook timed out, or the machine slept mid-turn. Such a lease is downgraded
|
|
29
|
+
* to `idle` rather than dropped, because the session itself is very likely
|
|
30
|
+
* still alive.
|
|
31
|
+
*/
|
|
32
|
+
const TRANSIENT_LEASE_MAX_AGE_MS = 10 * 60 * 1000
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Absolute ceiling. Unlike OpenCode, which heartbeats every 5s, hooks are
|
|
36
|
+
* event-driven: an idle session legitimately writes nothing for hours, so age
|
|
37
|
+
* alone cannot mean "dead". Liveness comes from the pid check; this only stops
|
|
38
|
+
* a lease surviving forever when the pid was never resolvable or got reused.
|
|
39
|
+
*/
|
|
40
|
+
const LEASE_HARD_MAX_AGE_MS = 24 * 60 * 60 * 1000
|
|
41
|
+
|
|
42
|
+
export const claudeInstanceDir = (): string =>
|
|
43
|
+
join(
|
|
44
|
+
process.env["XDG_STATE_HOME"] ?? join(homedir(), ".local", "state"),
|
|
45
|
+
"sirenodeck",
|
|
46
|
+
"coding-agents",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
export const claudeLeaseName = (sessionId: string): string =>
|
|
50
|
+
`claude-${sessionId}.json`
|
|
51
|
+
|
|
52
|
+
const processIsAlive = (pid: number): boolean => {
|
|
53
|
+
try {
|
|
54
|
+
process.kill(pid, 0)
|
|
55
|
+
return true
|
|
56
|
+
} catch {
|
|
57
|
+
return false
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const parseStatus = (value: unknown): AgentStatus | null =>
|
|
62
|
+
value === "idle" ||
|
|
63
|
+
value === "running" ||
|
|
64
|
+
value === "waiting" ||
|
|
65
|
+
value === "waiting_for_human" ||
|
|
66
|
+
value === "error" ||
|
|
67
|
+
value === "compacting"
|
|
68
|
+
? value
|
|
69
|
+
: null
|
|
70
|
+
|
|
71
|
+
const parseLease = (raw: string, now: number): ClaudeInstance | null => {
|
|
72
|
+
let value: Record<string, unknown>
|
|
73
|
+
try {
|
|
74
|
+
value = JSON.parse(raw) as Record<string, unknown>
|
|
75
|
+
} catch {
|
|
76
|
+
return null
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const sessionId = value["sessionId"]
|
|
80
|
+
const cwd = value["cwd"]
|
|
81
|
+
const updatedAt = value["updatedAt"]
|
|
82
|
+
let status = parseStatus(value["state"])
|
|
83
|
+
if (
|
|
84
|
+
typeof sessionId !== "string" ||
|
|
85
|
+
sessionId.length === 0 ||
|
|
86
|
+
typeof cwd !== "string" ||
|
|
87
|
+
typeof updatedAt !== "number" ||
|
|
88
|
+
status === null
|
|
89
|
+
) {
|
|
90
|
+
return null
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const rawPid = value["pid"]
|
|
94
|
+
const pid =
|
|
95
|
+
typeof rawPid === "number" && Number.isInteger(rawPid) && rawPid > 0
|
|
96
|
+
? rawPid
|
|
97
|
+
: null
|
|
98
|
+
|
|
99
|
+
const age = now - updatedAt
|
|
100
|
+
if (age > LEASE_HARD_MAX_AGE_MS) return null
|
|
101
|
+
// The hook could not find the claude process, so there is nothing to probe;
|
|
102
|
+
// fall back to the age ceiling above rather than suppressing the session.
|
|
103
|
+
if (pid !== null && !processIsAlive(pid)) return null
|
|
104
|
+
|
|
105
|
+
if (
|
|
106
|
+
(status === "running" || status === "compacting") &&
|
|
107
|
+
age > TRANSIENT_LEASE_MAX_AGE_MS
|
|
108
|
+
) {
|
|
109
|
+
status = "idle"
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const transcriptPath = value["transcriptPath"]
|
|
113
|
+
return {
|
|
114
|
+
instanceId: `claude:${sessionId}`,
|
|
115
|
+
sessionId,
|
|
116
|
+
pid,
|
|
117
|
+
cwd,
|
|
118
|
+
status,
|
|
119
|
+
updatedAt,
|
|
120
|
+
...(typeof transcriptPath === "string" && transcriptPath.length > 0
|
|
121
|
+
? { transcriptPath }
|
|
122
|
+
: {}),
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface ReadClaudeInstancesOptions {
|
|
127
|
+
readonly dir?: string
|
|
128
|
+
readonly now?: number
|
|
129
|
+
/** Remove leases whose process is gone. The daemon is the only reader. */
|
|
130
|
+
readonly prune?: boolean
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export const readClaudeInstances = async (
|
|
134
|
+
opts: ReadClaudeInstancesOptions = {},
|
|
135
|
+
): Promise<readonly ClaudeInstance[]> => {
|
|
136
|
+
const dir = opts.dir ?? claudeInstanceDir()
|
|
137
|
+
const now = opts.now ?? Date.now()
|
|
138
|
+
|
|
139
|
+
let names: string[]
|
|
140
|
+
try {
|
|
141
|
+
names = (await readdir(dir)).filter(
|
|
142
|
+
(name) => name.startsWith("claude-") && name.endsWith(".json"),
|
|
143
|
+
)
|
|
144
|
+
} catch {
|
|
145
|
+
return []
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const instances: ClaudeInstance[] = []
|
|
149
|
+
for (const name of names) {
|
|
150
|
+
const full = join(dir, name)
|
|
151
|
+
let instance: ClaudeInstance | null = null
|
|
152
|
+
try {
|
|
153
|
+
instance = parseLease(await readFile(full, "utf8"), now)
|
|
154
|
+
} catch {
|
|
155
|
+
// A session may replace or remove its lease while the dir is read.
|
|
156
|
+
continue
|
|
157
|
+
}
|
|
158
|
+
if (instance === null) {
|
|
159
|
+
if (opts.prune === true) {
|
|
160
|
+
try {
|
|
161
|
+
await unlink(full)
|
|
162
|
+
} catch {
|
|
163
|
+
// Already gone, or not ours to remove.
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
continue
|
|
167
|
+
}
|
|
168
|
+
// ponytail: hooks fire at turn boundaries, so between PreToolUse and Stop
|
|
169
|
+
// the lease goes quiet while the session is very much working. The
|
|
170
|
+
// transcript's mtime is a free heartbeat that keeps "last activity"
|
|
171
|
+
// honest without asking the hook to poll.
|
|
172
|
+
let updatedAt = instance.updatedAt
|
|
173
|
+
if (instance.transcriptPath !== undefined) {
|
|
174
|
+
try {
|
|
175
|
+
const st = await stat(instance.transcriptPath)
|
|
176
|
+
updatedAt = Math.max(updatedAt, st.mtimeMs)
|
|
177
|
+
} catch {
|
|
178
|
+
// Transcript moved or removed; the lease timestamp stands.
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
instances.push({ ...instance, updatedAt })
|
|
182
|
+
}
|
|
183
|
+
return instances.sort((a, b) => b.updatedAt - a.updatedAt)
|
|
184
|
+
}
|