@oddessentials/agent-guild 0.1.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 +162 -0
- package/bin/agent-guild-report.mjs +134 -0
- package/bin/agent-guild.mjs +240 -0
- package/config/providers.default.json +124 -0
- package/examples/claude-code-settings.json +48 -0
- package/examples/codex-hooks.json +45 -0
- package/examples/gemini-settings.json +61 -0
- package/examples/grok-hooks.json +55 -0
- package/examples/providers.json +27 -0
- package/node_modules/@xterm/addon-fit/LICENSE +19 -0
- package/node_modules/@xterm/addon-fit/README.md +24 -0
- package/node_modules/@xterm/addon-fit/lib/addon-fit.js +2 -0
- package/node_modules/@xterm/addon-fit/lib/addon-fit.js.map +1 -0
- package/node_modules/@xterm/addon-fit/lib/addon-fit.mjs +18 -0
- package/node_modules/@xterm/addon-fit/lib/addon-fit.mjs.map +7 -0
- package/node_modules/@xterm/addon-fit/package.json +26 -0
- package/node_modules/@xterm/addon-fit/src/FitAddon.ts +92 -0
- package/node_modules/@xterm/addon-fit/typings/addon-fit.d.ts +55 -0
- package/node_modules/@xterm/addon-serialize/README.md +42 -0
- package/node_modules/@xterm/addon-serialize/lib/addon-serialize.js +2 -0
- package/node_modules/@xterm/addon-serialize/lib/addon-serialize.js.map +1 -0
- package/node_modules/@xterm/addon-serialize/lib/addon-serialize.mjs +19 -0
- package/node_modules/@xterm/addon-serialize/lib/addon-serialize.mjs.map +7 -0
- package/node_modules/@xterm/addon-serialize/package.json +29 -0
- package/node_modules/@xterm/addon-serialize/src/SerializeAddon.ts +695 -0
- package/node_modules/@xterm/addon-serialize/typings/addon-serialize.d.ts +116 -0
- package/node_modules/@xterm/addon-web-links/LICENSE +19 -0
- package/node_modules/@xterm/addon-web-links/README.md +21 -0
- package/node_modules/@xterm/addon-web-links/lib/addon-web-links.js +2 -0
- package/node_modules/@xterm/addon-web-links/lib/addon-web-links.js.map +1 -0
- package/node_modules/@xterm/addon-web-links/lib/addon-web-links.mjs +18 -0
- package/node_modules/@xterm/addon-web-links/lib/addon-web-links.mjs.map +7 -0
- package/node_modules/@xterm/addon-web-links/package.json +26 -0
- package/node_modules/@xterm/addon-web-links/src/WebLinkProvider.ts +199 -0
- package/node_modules/@xterm/addon-web-links/src/WebLinksAddon.ts +58 -0
- package/node_modules/@xterm/addon-web-links/typings/addon-web-links.d.ts +57 -0
- package/node_modules/@xterm/headless/README.md +31 -0
- package/node_modules/@xterm/headless/lib-headless/xterm-headless.js +2 -0
- package/node_modules/@xterm/headless/lib-headless/xterm-headless.js.map +1 -0
- package/node_modules/@xterm/headless/lib-headless/xterm-headless.mjs +46 -0
- package/node_modules/@xterm/headless/lib-headless/xterm-headless.mjs.map +7 -0
- package/node_modules/@xterm/headless/package.json +29 -0
- package/node_modules/@xterm/headless/typings/xterm-headless.d.ts +1385 -0
- package/node_modules/@xterm/xterm/LICENSE +21 -0
- package/node_modules/@xterm/xterm/README.md +243 -0
- package/node_modules/@xterm/xterm/css/xterm.css +285 -0
- package/node_modules/@xterm/xterm/lib/xterm.js +2 -0
- package/node_modules/@xterm/xterm/lib/xterm.js.map +1 -0
- package/node_modules/@xterm/xterm/lib/xterm.mjs +53 -0
- package/node_modules/@xterm/xterm/lib/xterm.mjs.map +7 -0
- package/node_modules/@xterm/xterm/package.json +111 -0
- package/node_modules/@xterm/xterm/src/browser/AccessibilityManager.ts +435 -0
- package/node_modules/@xterm/xterm/src/browser/Clipboard.ts +93 -0
- package/node_modules/@xterm/xterm/src/browser/ColorContrastCache.ts +34 -0
- package/node_modules/@xterm/xterm/src/browser/CoreBrowserTerminal.ts +1339 -0
- package/node_modules/@xterm/xterm/src/browser/Linkifier.ts +403 -0
- package/node_modules/@xterm/xterm/src/browser/LocalizableStrings.ts +23 -0
- package/node_modules/@xterm/xterm/src/browser/OscLinkProvider.ts +129 -0
- package/node_modules/@xterm/xterm/src/browser/RenderDebouncer.ts +84 -0
- package/node_modules/@xterm/xterm/src/browser/TimeBasedDebouncer.ts +86 -0
- package/node_modules/@xterm/xterm/src/browser/Types.ts +226 -0
- package/node_modules/@xterm/xterm/src/browser/Viewport.ts +192 -0
- package/node_modules/@xterm/xterm/src/browser/decorations/BufferDecorationRenderer.ts +139 -0
- package/node_modules/@xterm/xterm/src/browser/decorations/ColorZoneStore.ts +117 -0
- package/node_modules/@xterm/xterm/src/browser/decorations/OverviewRulerRenderer.ts +214 -0
- package/node_modules/@xterm/xterm/src/browser/input/CompositionHelper.ts +248 -0
- package/node_modules/@xterm/xterm/src/browser/input/Mouse.ts +54 -0
- package/node_modules/@xterm/xterm/src/browser/input/MoveToCell.ts +251 -0
- package/node_modules/@xterm/xterm/src/browser/public/Terminal.ts +275 -0
- package/node_modules/@xterm/xterm/src/browser/renderer/dom/DomRenderer.ts +542 -0
- package/node_modules/@xterm/xterm/src/browser/renderer/dom/DomRendererRowFactory.ts +546 -0
- package/node_modules/@xterm/xterm/src/browser/renderer/dom/WidthCache.ts +167 -0
- package/node_modules/@xterm/xterm/src/browser/renderer/shared/Constants.ts +6 -0
- package/node_modules/@xterm/xterm/src/browser/renderer/shared/RendererUtils.ts +95 -0
- package/node_modules/@xterm/xterm/src/browser/renderer/shared/SelectionRenderModel.ts +93 -0
- package/node_modules/@xterm/xterm/src/browser/renderer/shared/Types.ts +84 -0
- package/node_modules/@xterm/xterm/src/browser/selection/SelectionModel.ts +144 -0
- package/node_modules/@xterm/xterm/src/browser/selection/Types.ts +15 -0
- package/node_modules/@xterm/xterm/src/browser/services/CharSizeService.ts +127 -0
- package/node_modules/@xterm/xterm/src/browser/services/CharacterJoinerService.ts +339 -0
- package/node_modules/@xterm/xterm/src/browser/services/CoreBrowserService.ts +137 -0
- package/node_modules/@xterm/xterm/src/browser/services/LinkProviderService.ts +28 -0
- package/node_modules/@xterm/xterm/src/browser/services/MouseService.ts +46 -0
- package/node_modules/@xterm/xterm/src/browser/services/RenderService.ts +376 -0
- package/node_modules/@xterm/xterm/src/browser/services/SelectionService.ts +1039 -0
- package/node_modules/@xterm/xterm/src/browser/services/Services.ts +158 -0
- package/node_modules/@xterm/xterm/src/browser/services/ThemeService.ts +198 -0
- package/node_modules/@xterm/xterm/src/browser/shared/Constants.ts +8 -0
- package/node_modules/@xterm/xterm/src/common/CircularList.ts +241 -0
- package/node_modules/@xterm/xterm/src/common/Clone.ts +23 -0
- package/node_modules/@xterm/xterm/src/common/Color.ts +376 -0
- package/node_modules/@xterm/xterm/src/common/CoreTerminal.ts +283 -0
- package/node_modules/@xterm/xterm/src/common/InputHandler.ts +3495 -0
- package/node_modules/@xterm/xterm/src/common/MultiKeyMap.ts +42 -0
- package/node_modules/@xterm/xterm/src/common/Platform.ts +44 -0
- package/node_modules/@xterm/xterm/src/common/SortedList.ts +194 -0
- package/node_modules/@xterm/xterm/src/common/TaskQueue.ts +166 -0
- package/node_modules/@xterm/xterm/src/common/TypedArrayUtils.ts +17 -0
- package/node_modules/@xterm/xterm/src/common/Types.ts +552 -0
- package/node_modules/@xterm/xterm/src/common/WindowsMode.ts +27 -0
- package/node_modules/@xterm/xterm/src/common/buffer/AttributeData.ts +211 -0
- package/node_modules/@xterm/xterm/src/common/buffer/Buffer.ts +662 -0
- package/node_modules/@xterm/xterm/src/common/buffer/BufferLine.ts +551 -0
- package/node_modules/@xterm/xterm/src/common/buffer/BufferRange.ts +13 -0
- package/node_modules/@xterm/xterm/src/common/buffer/BufferReflow.ts +226 -0
- package/node_modules/@xterm/xterm/src/common/buffer/BufferSet.ts +134 -0
- package/node_modules/@xterm/xterm/src/common/buffer/CellData.ts +94 -0
- package/node_modules/@xterm/xterm/src/common/buffer/Constants.ts +157 -0
- package/node_modules/@xterm/xterm/src/common/buffer/Marker.ts +43 -0
- package/node_modules/@xterm/xterm/src/common/buffer/Types.ts +52 -0
- package/node_modules/@xterm/xterm/src/common/data/Charsets.ts +256 -0
- package/node_modules/@xterm/xterm/src/common/data/EscapeSequences.ts +153 -0
- package/node_modules/@xterm/xterm/src/common/input/Keyboard.ts +373 -0
- package/node_modules/@xterm/xterm/src/common/input/TextDecoder.ts +346 -0
- package/node_modules/@xterm/xterm/src/common/input/UnicodeV6.ts +145 -0
- package/node_modules/@xterm/xterm/src/common/input/WriteBuffer.ts +247 -0
- package/node_modules/@xterm/xterm/src/common/input/XParseColor.ts +80 -0
- package/node_modules/@xterm/xterm/src/common/parser/Constants.ts +58 -0
- package/node_modules/@xterm/xterm/src/common/parser/DcsParser.ts +192 -0
- package/node_modules/@xterm/xterm/src/common/parser/EscapeSequenceParser.ts +792 -0
- package/node_modules/@xterm/xterm/src/common/parser/OscParser.ts +238 -0
- package/node_modules/@xterm/xterm/src/common/parser/Params.ts +229 -0
- package/node_modules/@xterm/xterm/src/common/parser/Types.ts +275 -0
- package/node_modules/@xterm/xterm/src/common/public/AddonManager.ts +53 -0
- package/node_modules/@xterm/xterm/src/common/public/BufferApiView.ts +35 -0
- package/node_modules/@xterm/xterm/src/common/public/BufferLineApiView.ts +29 -0
- package/node_modules/@xterm/xterm/src/common/public/BufferNamespaceApi.ts +36 -0
- package/node_modules/@xterm/xterm/src/common/public/ParserApi.ts +37 -0
- package/node_modules/@xterm/xterm/src/common/public/UnicodeApi.ts +27 -0
- package/node_modules/@xterm/xterm/src/common/services/BufferService.ts +154 -0
- package/node_modules/@xterm/xterm/src/common/services/CharsetService.ts +34 -0
- package/node_modules/@xterm/xterm/src/common/services/CoreMouseService.ts +365 -0
- package/node_modules/@xterm/xterm/src/common/services/CoreService.ts +92 -0
- package/node_modules/@xterm/xterm/src/common/services/DecorationService.ts +140 -0
- package/node_modules/@xterm/xterm/src/common/services/InstantiationService.ts +85 -0
- package/node_modules/@xterm/xterm/src/common/services/LogService.ts +124 -0
- package/node_modules/@xterm/xterm/src/common/services/OptionsService.ts +212 -0
- package/node_modules/@xterm/xterm/src/common/services/OscLinkService.ts +115 -0
- package/node_modules/@xterm/xterm/src/common/services/ServiceRegistry.ts +49 -0
- package/node_modules/@xterm/xterm/src/common/services/Services.ts +396 -0
- package/node_modules/@xterm/xterm/src/common/services/UnicodeService.ts +111 -0
- package/node_modules/@xterm/xterm/src/vs/base/browser/browser.ts +141 -0
- package/node_modules/@xterm/xterm/src/vs/base/browser/canIUse.ts +49 -0
- package/node_modules/@xterm/xterm/src/vs/base/browser/dom.ts +2369 -0
- package/node_modules/@xterm/xterm/src/vs/base/browser/fastDomNode.ts +316 -0
- package/node_modules/@xterm/xterm/src/vs/base/browser/globalPointerMoveMonitor.ts +112 -0
- package/node_modules/@xterm/xterm/src/vs/base/browser/iframe.ts +135 -0
- package/node_modules/@xterm/xterm/src/vs/base/browser/keyboardEvent.ts +213 -0
- package/node_modules/@xterm/xterm/src/vs/base/browser/mouseEvent.ts +229 -0
- package/node_modules/@xterm/xterm/src/vs/base/browser/touch.ts +372 -0
- package/node_modules/@xterm/xterm/src/vs/base/browser/ui/scrollbar/abstractScrollbar.ts +303 -0
- package/node_modules/@xterm/xterm/src/vs/base/browser/ui/scrollbar/horizontalScrollbar.ts +114 -0
- package/node_modules/@xterm/xterm/src/vs/base/browser/ui/scrollbar/scrollableElement.ts +720 -0
- package/node_modules/@xterm/xterm/src/vs/base/browser/ui/scrollbar/scrollableElementOptions.ts +165 -0
- package/node_modules/@xterm/xterm/src/vs/base/browser/ui/scrollbar/scrollbarArrow.ts +114 -0
- package/node_modules/@xterm/xterm/src/vs/base/browser/ui/scrollbar/scrollbarState.ts +243 -0
- package/node_modules/@xterm/xterm/src/vs/base/browser/ui/scrollbar/scrollbarVisibilityController.ts +118 -0
- package/node_modules/@xterm/xterm/src/vs/base/browser/ui/scrollbar/verticalScrollbar.ts +116 -0
- package/node_modules/@xterm/xterm/src/vs/base/browser/ui/widget.ts +57 -0
- package/node_modules/@xterm/xterm/src/vs/base/browser/window.ts +14 -0
- package/node_modules/@xterm/xterm/src/vs/base/common/arrays.ts +887 -0
- package/node_modules/@xterm/xterm/src/vs/base/common/arraysFind.ts +202 -0
- package/node_modules/@xterm/xterm/src/vs/base/common/assert.ts +71 -0
- package/node_modules/@xterm/xterm/src/vs/base/common/async.ts +1992 -0
- package/node_modules/@xterm/xterm/src/vs/base/common/cancellation.ts +148 -0
- package/node_modules/@xterm/xterm/src/vs/base/common/charCode.ts +450 -0
- package/node_modules/@xterm/xterm/src/vs/base/common/collections.ts +140 -0
- package/node_modules/@xterm/xterm/src/vs/base/common/decorators.ts +130 -0
- package/node_modules/@xterm/xterm/src/vs/base/common/equals.ts +146 -0
- package/node_modules/@xterm/xterm/src/vs/base/common/errors.ts +303 -0
- package/node_modules/@xterm/xterm/src/vs/base/common/event.ts +1778 -0
- package/node_modules/@xterm/xterm/src/vs/base/common/functional.ts +32 -0
- package/node_modules/@xterm/xterm/src/vs/base/common/hash.ts +316 -0
- package/node_modules/@xterm/xterm/src/vs/base/common/iterator.ts +159 -0
- package/node_modules/@xterm/xterm/src/vs/base/common/keyCodes.ts +526 -0
- package/node_modules/@xterm/xterm/src/vs/base/common/keybindings.ts +284 -0
- package/node_modules/@xterm/xterm/src/vs/base/common/lazy.ts +47 -0
- package/node_modules/@xterm/xterm/src/vs/base/common/lifecycle.ts +801 -0
- package/node_modules/@xterm/xterm/src/vs/base/common/linkedList.ts +142 -0
- package/node_modules/@xterm/xterm/src/vs/base/common/map.ts +202 -0
- package/node_modules/@xterm/xterm/src/vs/base/common/numbers.ts +98 -0
- package/node_modules/@xterm/xterm/src/vs/base/common/observable.ts +76 -0
- package/node_modules/@xterm/xterm/src/vs/base/common/observableInternal/api.ts +31 -0
- package/node_modules/@xterm/xterm/src/vs/base/common/observableInternal/autorun.ts +281 -0
- package/node_modules/@xterm/xterm/src/vs/base/common/observableInternal/base.ts +489 -0
- package/node_modules/@xterm/xterm/src/vs/base/common/observableInternal/debugName.ts +145 -0
- package/node_modules/@xterm/xterm/src/vs/base/common/observableInternal/derived.ts +428 -0
- package/node_modules/@xterm/xterm/src/vs/base/common/observableInternal/lazyObservableValue.ts +146 -0
- package/node_modules/@xterm/xterm/src/vs/base/common/observableInternal/logging.ts +328 -0
- package/node_modules/@xterm/xterm/src/vs/base/common/observableInternal/promise.ts +209 -0
- package/node_modules/@xterm/xterm/src/vs/base/common/observableInternal/utils.ts +610 -0
- package/node_modules/@xterm/xterm/src/vs/base/common/platform.ts +281 -0
- package/node_modules/@xterm/xterm/src/vs/base/common/scrollable.ts +522 -0
- package/node_modules/@xterm/xterm/src/vs/base/common/sequence.ts +34 -0
- package/node_modules/@xterm/xterm/src/vs/base/common/stopwatch.ts +43 -0
- package/node_modules/@xterm/xterm/src/vs/base/common/strings.ts +557 -0
- package/node_modules/@xterm/xterm/src/vs/base/common/symbols.ts +9 -0
- package/node_modules/@xterm/xterm/src/vs/base/common/uint.ts +59 -0
- package/node_modules/@xterm/xterm/src/vs/patches/nls.ts +90 -0
- package/node_modules/@xterm/xterm/src/vs/typings/base-common.d.ts +20 -0
- package/node_modules/@xterm/xterm/src/vs/typings/require.d.ts +42 -0
- package/node_modules/@xterm/xterm/src/vs/typings/vscode-globals-nls.d.ts +36 -0
- package/node_modules/@xterm/xterm/src/vs/typings/vscode-globals-product.d.ts +33 -0
- package/node_modules/@xterm/xterm/typings/xterm.d.ts +1957 -0
- package/node_modules/node-addon-api/LICENSE.md +9 -0
- package/node_modules/node-addon-api/README.md +319 -0
- package/node_modules/node-addon-api/common.gypi +20 -0
- package/node_modules/node-addon-api/except.gypi +25 -0
- package/node_modules/node-addon-api/index.js +12 -0
- package/node_modules/node-addon-api/napi-inl.deprecated.h +186 -0
- package/node_modules/node-addon-api/napi-inl.h +6607 -0
- package/node_modules/node-addon-api/napi.h +3201 -0
- package/node_modules/node-addon-api/node_addon_api.gyp +32 -0
- package/node_modules/node-addon-api/node_api.gyp +9 -0
- package/node_modules/node-addon-api/noexcept.gypi +26 -0
- package/node_modules/node-addon-api/nothing.c +0 -0
- package/node_modules/node-addon-api/package-support.json +21 -0
- package/node_modules/node-addon-api/package.json +480 -0
- package/node_modules/node-addon-api/tools/README.md +73 -0
- package/node_modules/node-addon-api/tools/check-napi.js +99 -0
- package/node_modules/node-addon-api/tools/clang-format.js +71 -0
- package/node_modules/node-addon-api/tools/conversion.js +301 -0
- package/node_modules/node-addon-api/tools/eslint-format.js +79 -0
- package/node_modules/node-pty/LICENSE +69 -0
- package/node_modules/node-pty/README.md +164 -0
- package/node_modules/node-pty/binding.gyp +150 -0
- package/node_modules/node-pty/lib/conpty_console_list_agent.js +25 -0
- package/node_modules/node-pty/lib/eventEmitter2.js +47 -0
- package/node_modules/node-pty/lib/index.js +52 -0
- package/node_modules/node-pty/lib/interfaces.js +7 -0
- package/node_modules/node-pty/lib/shared/conout.js +10 -0
- package/node_modules/node-pty/lib/terminal.js +190 -0
- package/node_modules/node-pty/lib/types.js +7 -0
- package/node_modules/node-pty/lib/unixTerminal.js +351 -0
- package/node_modules/node-pty/lib/utils.js +38 -0
- package/node_modules/node-pty/lib/windowsConoutConnection.js +131 -0
- package/node_modules/node-pty/lib/windowsPtyAgent.js +344 -0
- package/node_modules/node-pty/lib/windowsTerminal.js +209 -0
- package/node_modules/node-pty/lib/worker/conoutSocketWorker.js +22 -0
- package/node_modules/node-pty/package.json +66 -0
- package/node_modules/node-pty/prebuilds/darwin-arm64/pty.node +0 -0
- package/node_modules/node-pty/prebuilds/darwin-arm64/spawn-helper +0 -0
- package/node_modules/node-pty/prebuilds/darwin-x64/pty.node +0 -0
- package/node_modules/node-pty/prebuilds/darwin-x64/spawn-helper +0 -0
- package/node_modules/node-pty/prebuilds/linux-arm64/pty.node +0 -0
- package/node_modules/node-pty/prebuilds/linux-x64/pty.node +0 -0
- package/node_modules/node-pty/prebuilds/win32-arm64/conpty/OpenConsole.exe +0 -0
- package/node_modules/node-pty/prebuilds/win32-arm64/conpty/conpty.dll +0 -0
- package/node_modules/node-pty/prebuilds/win32-arm64/conpty.node +0 -0
- package/node_modules/node-pty/prebuilds/win32-arm64/conpty.pdb +0 -0
- package/node_modules/node-pty/prebuilds/win32-arm64/conpty_console_list.node +0 -0
- package/node_modules/node-pty/prebuilds/win32-arm64/conpty_console_list.pdb +0 -0
- package/node_modules/node-pty/prebuilds/win32-x64/conpty/OpenConsole.exe +0 -0
- package/node_modules/node-pty/prebuilds/win32-x64/conpty/conpty.dll +0 -0
- package/node_modules/node-pty/prebuilds/win32-x64/conpty.node +0 -0
- package/node_modules/node-pty/prebuilds/win32-x64/conpty.pdb +0 -0
- package/node_modules/node-pty/prebuilds/win32-x64/conpty_console_list.node +0 -0
- package/node_modules/node-pty/prebuilds/win32-x64/conpty_console_list.pdb +0 -0
- package/node_modules/node-pty/scripts/post-install.js +76 -0
- package/node_modules/node-pty/scripts/prebuild.js +34 -0
- package/node_modules/node-pty/src/unix/pty.cc +876 -0
- package/node_modules/node-pty/src/unix/spawn-helper.cc +23 -0
- package/node_modules/node-pty/src/win/conpty.cc +616 -0
- package/node_modules/node-pty/src/win/conpty.h +41 -0
- package/node_modules/node-pty/src/win/conpty_console_list.cc +44 -0
- package/node_modules/node-pty/src/win/path_util.cc +95 -0
- package/node_modules/node-pty/src/win/path_util.h +26 -0
- package/node_modules/node-pty/third_party/conpty/1.25.260303002/win10-arm64/OpenConsole.exe +0 -0
- package/node_modules/node-pty/third_party/conpty/1.25.260303002/win10-arm64/conpty.dll +0 -0
- package/node_modules/node-pty/third_party/conpty/1.25.260303002/win10-x64/OpenConsole.exe +0 -0
- package/node_modules/node-pty/third_party/conpty/1.25.260303002/win10-x64/conpty.dll +0 -0
- package/node_modules/node-pty/typings/node-pty.d.ts +215 -0
- package/node_modules/ws/LICENSE +20 -0
- package/node_modules/ws/README.md +548 -0
- package/node_modules/ws/browser.js +8 -0
- package/node_modules/ws/index.js +22 -0
- package/node_modules/ws/lib/buffer-util.js +131 -0
- package/node_modules/ws/lib/constants.js +19 -0
- package/node_modules/ws/lib/event-target.js +292 -0
- package/node_modules/ws/lib/extension.js +203 -0
- package/node_modules/ws/lib/limiter.js +55 -0
- package/node_modules/ws/lib/permessage-deflate.js +530 -0
- package/node_modules/ws/lib/receiver.js +743 -0
- package/node_modules/ws/lib/sender.js +607 -0
- package/node_modules/ws/lib/stream.js +161 -0
- package/node_modules/ws/lib/subprotocol.js +62 -0
- package/node_modules/ws/lib/validation.js +152 -0
- package/node_modules/ws/lib/websocket-server.js +562 -0
- package/node_modules/ws/lib/websocket.js +1421 -0
- package/node_modules/ws/package.json +74 -0
- package/node_modules/ws/wrapper.mjs +21 -0
- package/package.json +52 -0
- package/src/manager/command-resolver.mjs +140 -0
- package/src/manager/config.mjs +94 -0
- package/src/manager/install-channels.mjs +268 -0
- package/src/manager/main.mjs +136 -0
- package/src/manager/providers.mjs +601 -0
- package/src/manager/report-shims.mjs +104 -0
- package/src/manager/server.mjs +421 -0
- package/src/manager/session-manager.mjs +306 -0
- package/src/manager/session.mjs +531 -0
- package/src/manager/shell-env.mjs +150 -0
- package/src/manager/usage.mjs +535 -0
- package/src/manager/versions.mjs +62 -0
- package/src/report/hooks.mjs +150 -0
- package/web/app.js +916 -0
- package/web/art/art.css +186 -0
- package/web/art/characters/anthropic/idle.png +0 -0
- package/web/art/characters/anthropic/locked.png +0 -0
- package/web/art/characters/anthropic/working.png +0 -0
- package/web/art/characters/google/idle.png +0 -0
- package/web/art/characters/google/locked.png +0 -0
- package/web/art/characters/google/working.png +0 -0
- package/web/art/characters/openai/idle.png +0 -0
- package/web/art/characters/openai/locked.png +0 -0
- package/web/art/characters/openai/working.png +0 -0
- package/web/art/characters/shell/idle.png +0 -0
- package/web/art/characters/shell/locked.png +0 -0
- package/web/art/characters/shell/working.png +0 -0
- package/web/art/characters/xai/idle.png +0 -0
- package/web/art/characters/xai/locked.png +0 -0
- package/web/art/characters/xai/working.png +0 -0
- package/web/art/familiars/aether.png +0 -0
- package/web/art/familiars/flame.png +0 -0
- package/web/art/familiars/leaf.png +0 -0
- package/web/art/familiars/night.png +0 -0
- package/web/art/icons/anthropic.png +0 -0
- package/web/art/icons/google.png +0 -0
- package/web/art/icons/openai.png +0 -0
- package/web/art/icons/shell.png +0 -0
- package/web/art/icons/xai.png +0 -0
- package/web/art/page.png +0 -0
- package/web/art/ui/frame.png +0 -0
- package/web/art/ui/gem-mana.png +0 -0
- package/web/art/ui/gem-vitality.png +0 -0
- package/web/art/ui/guild-crest.png +0 -0
- package/web/art/ui/level-medallion.png +0 -0
- package/web/art/ui/padlock.png +0 -0
- package/web/icons/README.md +8 -0
- package/web/index.html +154 -0
- package/web/styles.css +226 -0
package/web/app.js
ADDED
|
@@ -0,0 +1,916 @@
|
|
|
1
|
+
// Agent Guild web page. A thin client of the session manager's local API:
|
|
2
|
+
// it never owns sessions, so closing the page leaves them running.
|
|
3
|
+
|
|
4
|
+
const TOKEN_KEY = 'agentGuild.token';
|
|
5
|
+
const CWD_KEY = 'agentGuild.cwd';
|
|
6
|
+
const isMac = /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent);
|
|
7
|
+
|
|
8
|
+
const $ = (id) => document.getElementById(id);
|
|
9
|
+
const state = {
|
|
10
|
+
token: null,
|
|
11
|
+
providers: [],
|
|
12
|
+
usage: new Map(),
|
|
13
|
+
sessions: new Map(),
|
|
14
|
+
views: new Map(),
|
|
15
|
+
activeId: null,
|
|
16
|
+
eventsSocket: null,
|
|
17
|
+
eventsRetry: 0,
|
|
18
|
+
/** True from a shutdown request until the manager is reachable again. */
|
|
19
|
+
stopping: false,
|
|
20
|
+
/** After a stop: how many session processes did not confirm exiting, or null if the manager never said. */
|
|
21
|
+
stopRemaining: null,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// ---- storage (may be unavailable, e.g. blocked site data) -----------------
|
|
25
|
+
|
|
26
|
+
function load(key) { try { return localStorage.getItem(key); } catch { return null; } }
|
|
27
|
+
function save(key, value) { try { value === null ? localStorage.removeItem(key) : localStorage.setItem(key, value); } catch { /* ignore */ } }
|
|
28
|
+
|
|
29
|
+
// ---- helpers --------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
let toastTimer;
|
|
32
|
+
function toast(message, ms = 5000) {
|
|
33
|
+
const el = $('toast');
|
|
34
|
+
el.textContent = message;
|
|
35
|
+
el.hidden = false;
|
|
36
|
+
clearTimeout(toastTimer);
|
|
37
|
+
toastTimer = setTimeout(() => { el.hidden = true; }, ms);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function setConnection(kind, label) {
|
|
41
|
+
const el = $('connection');
|
|
42
|
+
el.className = `connection ${kind}`;
|
|
43
|
+
el.querySelector('.label').textContent = label;
|
|
44
|
+
// The manager can only be stopped while the page can reach it.
|
|
45
|
+
$('stop-manager').hidden = kind !== 'ok';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function relativeTime(iso) {
|
|
49
|
+
if (!iso) return '';
|
|
50
|
+
const s = Math.max(0, Math.round((Date.now() - Date.parse(iso)) / 1000));
|
|
51
|
+
if (s < 60) return 'just now';
|
|
52
|
+
const m = Math.round(s / 60);
|
|
53
|
+
if (m < 60) return `${m} min ago`;
|
|
54
|
+
const h = Math.round(m / 60);
|
|
55
|
+
if (h < 24) return `${h} h ago`;
|
|
56
|
+
return `${Math.round(h / 24)} d ago`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function untilTime(iso) {
|
|
60
|
+
if (!iso) return '';
|
|
61
|
+
const s = Math.round((Date.parse(iso) - Date.now()) / 1000);
|
|
62
|
+
if (!Number.isFinite(s) || s <= 0) return 'resets now';
|
|
63
|
+
const h = Math.floor(s / 3600);
|
|
64
|
+
const m = Math.round((s % 3600) / 60);
|
|
65
|
+
if (h >= 48) return `resets in ${Math.round(h / 24)} d`;
|
|
66
|
+
if (h > 0) return `resets in ${h} h${m ? ` ${m} min` : ''}`;
|
|
67
|
+
return `resets in ${Math.max(1, m)} min`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function hueFor(text) {
|
|
71
|
+
let h = 0;
|
|
72
|
+
for (const ch of String(text)) h = (h * 31 + ch.codePointAt(0)) % 360;
|
|
73
|
+
return h;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function httpsHref(value) {
|
|
77
|
+
try {
|
|
78
|
+
const url = new URL(value);
|
|
79
|
+
return url.protocol === 'https:' ? url.href : null;
|
|
80
|
+
} catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function planLabel(plan) {
|
|
86
|
+
const text = String(plan ?? '').trim();
|
|
87
|
+
if (/[A-Z]/.test(text)) return text;
|
|
88
|
+
return text.replace(/[_-]+/g, ' ').replace(/\b[a-z]/g, (c) => c.toUpperCase());
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function paintProviderIcon(el, provider) {
|
|
92
|
+
el.style.setProperty('--c', provider.color || '#64748b');
|
|
93
|
+
el.dataset.provider = provider.id;
|
|
94
|
+
if (provider.iconUrl) {
|
|
95
|
+
el.classList.add('has-image');
|
|
96
|
+
el.style.backgroundImage = `url(${JSON.stringify(provider.iconUrl)})`;
|
|
97
|
+
el.textContent = '';
|
|
98
|
+
} else {
|
|
99
|
+
el.classList.remove('has-image');
|
|
100
|
+
el.style.backgroundImage = '';
|
|
101
|
+
el.textContent = provider.monogram || (provider.vendor || '?').charAt(0);
|
|
102
|
+
}
|
|
103
|
+
el.setAttribute('aria-hidden', 'true');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const FAMILIARS = ['flame', 'leaf', 'night', 'aether'];
|
|
107
|
+
|
|
108
|
+
function renderAgents(container, agents) {
|
|
109
|
+
container.replaceChildren(...agents.map((agent) => {
|
|
110
|
+
const el = document.createElement('span');
|
|
111
|
+
el.className = `agent ${agent.status}`;
|
|
112
|
+
el.style.setProperty('--c', `hsl(${hueFor(agent.name)} 65% 50%)`);
|
|
113
|
+
el.dataset.familiar = FAMILIARS[hueFor(agent.name) % FAMILIARS.length];
|
|
114
|
+
el.textContent = (agent.name || '?').charAt(0).toUpperCase();
|
|
115
|
+
const detail = agent.detail ? ` — ${agent.detail}` : '';
|
|
116
|
+
el.title = `${agent.name} (${agent.status})${detail}`;
|
|
117
|
+
el.setAttribute('role', 'img');
|
|
118
|
+
el.setAttribute('aria-label', el.title);
|
|
119
|
+
return el;
|
|
120
|
+
}));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ---- API ------------------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
class AuthError extends Error {}
|
|
126
|
+
|
|
127
|
+
async function api(method, path, body) {
|
|
128
|
+
const res = await fetch(`/api/v1${path}`, {
|
|
129
|
+
method,
|
|
130
|
+
headers: {
|
|
131
|
+
Authorization: `Bearer ${state.token}`,
|
|
132
|
+
...(body ? { 'Content-Type': 'application/json' } : {}),
|
|
133
|
+
},
|
|
134
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
135
|
+
});
|
|
136
|
+
if (res.status === 401) throw new AuthError('The access token was rejected.');
|
|
137
|
+
const data = await res.json().catch(() => ({}));
|
|
138
|
+
if (!res.ok) throw Object.assign(new Error(data?.error?.message || `Request failed (HTTP ${res.status})`), data?.error);
|
|
139
|
+
return data;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function wsUrl(path) {
|
|
143
|
+
const proto = location.protocol === 'https:' ? 'wss' : 'ws';
|
|
144
|
+
return `${proto}://${location.host}/api/v1${path}?token=${encodeURIComponent(state.token)}`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ---- providers ------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
function renderProviders() {
|
|
150
|
+
const list = $('providers');
|
|
151
|
+
const tpl = $('provider-template');
|
|
152
|
+
list.replaceChildren(...state.providers.map((provider) => {
|
|
153
|
+
const node = tpl.content.firstElementChild.cloneNode(true);
|
|
154
|
+
paintProviderIcon(node.querySelector('.provider-icon'), provider);
|
|
155
|
+
node.querySelector('.vendor').textContent = provider.vendor;
|
|
156
|
+
node.querySelector('.tool').textContent = provider.tool;
|
|
157
|
+
const stateLine = node.querySelector('.state');
|
|
158
|
+
stateLine.textContent = providerState(provider);
|
|
159
|
+
const checkFailed = provider.available && provider.versionStatus === 'failed';
|
|
160
|
+
stateLine.classList.toggle('update-available', provider.updateAvailable || checkFailed || Boolean(installNote(provider)));
|
|
161
|
+
stateLine.title = [provider.resolvedPath, provider.updateCommand && `Update: ${provider.updateCommand}`].filter(Boolean).join('\n');
|
|
162
|
+
node.dataset.id = provider.id;
|
|
163
|
+
node.classList.toggle('unavailable', !provider.available);
|
|
164
|
+
node.setAttribute('aria-label', `${provider.vendor} ${provider.tool}, ${!provider.available ? 'not installed' : checkFailed ? 'version check failed' : 'ready'}`);
|
|
165
|
+
const start = node.querySelector('.new');
|
|
166
|
+
const existing = node.querySelector('.existing');
|
|
167
|
+
const hint = node.querySelector('.hint');
|
|
168
|
+
start.hidden = !provider.available;
|
|
169
|
+
start.title = `Start a new ${provider.tool} session`;
|
|
170
|
+
start.addEventListener('click', () => startSession(provider, node));
|
|
171
|
+
existing.hidden = !provider.available || !provider.resumable;
|
|
172
|
+
existing.title = `Resume one of ${provider.tool}'s own sessions by its id`;
|
|
173
|
+
existing.addEventListener('click', () => resumeSession(provider, node));
|
|
174
|
+
const install = node.querySelector('.install');
|
|
175
|
+
install.hidden = provider.available || !provider.installable;
|
|
176
|
+
install.title = `Install ${provider.tool} using npm.${provider.npmNote ? ` ${provider.npmNote}` : ''}`;
|
|
177
|
+
install.addEventListener('click', () => installProvider(provider, node));
|
|
178
|
+
const update = node.querySelector('.update');
|
|
179
|
+
update.hidden = !(provider.available && provider.updateCommand && (provider.updateAvailable || checkFailed));
|
|
180
|
+
if (provider.installChannel !== 'npm') update.textContent = 'Update';
|
|
181
|
+
else update.textContent = checkFailed ? 'Reinstall' : `Update to ${provider.latestVersion}`;
|
|
182
|
+
update.title = provider.updateCommand ? `Run "${provider.updateCommand}" in a session` : '';
|
|
183
|
+
update.addEventListener('click', () => installProvider(provider, node));
|
|
184
|
+
renderHint(hint, provider);
|
|
185
|
+
renderCopies(node.querySelector('.copies'), provider);
|
|
186
|
+
renderConsoleLinks(node, provider);
|
|
187
|
+
renderUsage(node, provider);
|
|
188
|
+
return node;
|
|
189
|
+
}));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const openCopies = new Set();
|
|
193
|
+
|
|
194
|
+
function renderCopies(box, provider) {
|
|
195
|
+
const installs = provider.installs || [];
|
|
196
|
+
const warnings = provider.warnings || [];
|
|
197
|
+
box.hidden = warnings.length === 0;
|
|
198
|
+
if (box.hidden) return;
|
|
199
|
+
const inUse = installs.some((i) => i.active);
|
|
200
|
+
const older = installs.some((i) => i.newer);
|
|
201
|
+
box.querySelector('summary').textContent = !inUse
|
|
202
|
+
? 'A copy exists off PATH'
|
|
203
|
+
: `${installs.length} copies installed${older ? ' · older copy in use' : ''}`;
|
|
204
|
+
box.open = openCopies.has(provider.id);
|
|
205
|
+
box.addEventListener('toggle', () => (box.open ? openCopies.add(provider.id) : openCopies.delete(provider.id)));
|
|
206
|
+
const line = (className, ...content) => {
|
|
207
|
+
const span = document.createElement('span');
|
|
208
|
+
span.className = className;
|
|
209
|
+
span.append(...content);
|
|
210
|
+
return span;
|
|
211
|
+
};
|
|
212
|
+
box.querySelector('ul').replaceChildren(...warnings.map((text) => {
|
|
213
|
+
const item = document.createElement('li');
|
|
214
|
+
item.textContent = text;
|
|
215
|
+
return item;
|
|
216
|
+
}), ...installs.map((install) => {
|
|
217
|
+
const item = document.createElement('li');
|
|
218
|
+
const name = [CHANNEL_LABELS[install.channel] || install.channel, install.version && `v${install.version}`, install.active ? 'in use' : 'not in use'];
|
|
219
|
+
item.append(line('copy-name', name.filter(Boolean).join(' · ')), line('copy-path', install.path));
|
|
220
|
+
if (install.removeCommand) {
|
|
221
|
+
const code = document.createElement('code');
|
|
222
|
+
code.textContent = install.removeCommand;
|
|
223
|
+
item.append(line('copy-remove', 'To remove it: ', code));
|
|
224
|
+
} else {
|
|
225
|
+
item.append(line('copy-remove', 'No removal command is known for this copy.'));
|
|
226
|
+
}
|
|
227
|
+
return item;
|
|
228
|
+
}));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function renderHint(hint, provider) {
|
|
232
|
+
let text = '';
|
|
233
|
+
if (!provider.available) text = provider.installable ? '' : provider.install || `${provider.command} was not found on PATH.`;
|
|
234
|
+
else if (provider.versionStatus === 'failed') text = [provider.versionError, !provider.updateCommand && provider.updateGuidance].filter(Boolean).join(' ');
|
|
235
|
+
else if (provider.updateAvailable && !provider.updateCommand) text = provider.updateGuidance || '';
|
|
236
|
+
hint.hidden = !text;
|
|
237
|
+
hint.replaceChildren(text);
|
|
238
|
+
const docs = text && httpsHref(provider.docs);
|
|
239
|
+
if (!docs) return;
|
|
240
|
+
const link = document.createElement('a');
|
|
241
|
+
link.className = 'console-link';
|
|
242
|
+
link.href = docs;
|
|
243
|
+
link.target = '_blank';
|
|
244
|
+
link.rel = 'noopener noreferrer';
|
|
245
|
+
link.textContent = 'Docs ↗';
|
|
246
|
+
hint.append(' ', link);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function renderConsoleLinks(card, provider) {
|
|
250
|
+
let any = false;
|
|
251
|
+
for (const [selector, url, what] of [['.usage-link', provider.usageUrl, 'usage'], ['.billing-link', provider.billingUrl, 'billing']]) {
|
|
252
|
+
const link = card.querySelector(selector);
|
|
253
|
+
const href = httpsHref(url);
|
|
254
|
+
link.hidden = !href;
|
|
255
|
+
if (!href) { link.removeAttribute('href'); continue; }
|
|
256
|
+
any = true;
|
|
257
|
+
link.href = href;
|
|
258
|
+
link.title = `${provider.vendor} ${what} console: ${href}`;
|
|
259
|
+
link.setAttribute('aria-label', `${provider.vendor} ${what} console (opens in a new tab)`);
|
|
260
|
+
}
|
|
261
|
+
card.querySelector('.provider-links').hidden = !any;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function renderTier(card, provider, usage) {
|
|
265
|
+
const tier = card.querySelector('.tier');
|
|
266
|
+
const label = provider.available && usage?.plan ? planLabel(usage.plan) : '';
|
|
267
|
+
tier.hidden = !label;
|
|
268
|
+
tier.textContent = label;
|
|
269
|
+
tier.title = label ? `${provider.vendor} subscription: ${label}` : '';
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** One line for a prepaid credit balance, when the provider reports one. */
|
|
273
|
+
function creditsNote(usage) {
|
|
274
|
+
if (typeof usage?.credits !== 'number' || !Number.isFinite(usage.credits)) return [];
|
|
275
|
+
const note = document.createElement('div');
|
|
276
|
+
note.className = 'usage-note';
|
|
277
|
+
note.textContent = `Credits: ${usage.credits.toLocaleString(undefined, { maximumFractionDigits: 2 })} left`;
|
|
278
|
+
note.title = note.textContent;
|
|
279
|
+
return [note];
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function renderUsage(card, provider) {
|
|
283
|
+
const host = card.querySelector('.usage');
|
|
284
|
+
const usage = state.usage.get(provider.id);
|
|
285
|
+
renderTier(card, provider, provider.usageSource ? usage : null);
|
|
286
|
+
if (!provider.available || !provider.usageSource || !usage) return host.replaceChildren();
|
|
287
|
+
if (usage.error || usage.windows.length === 0) {
|
|
288
|
+
const note = document.createElement('div');
|
|
289
|
+
note.className = 'usage-note';
|
|
290
|
+
note.textContent = `Usage: ${usage.error || 'no limits reported'}`;
|
|
291
|
+
note.title = note.textContent;
|
|
292
|
+
return host.replaceChildren(note, ...creditsNote(usage));
|
|
293
|
+
}
|
|
294
|
+
host.replaceChildren(...usage.windows.map((w) => {
|
|
295
|
+
const node = $('meter-template').content.firstElementChild.cloneNode(true);
|
|
296
|
+
const left = Math.max(0, Math.round(100 - w.usedPercent));
|
|
297
|
+
node.classList.toggle('low', left <= 25 && left > 10);
|
|
298
|
+
node.classList.toggle('empty', left <= 10);
|
|
299
|
+
node.querySelector('.meter-label').textContent = w.label;
|
|
300
|
+
node.querySelector('.meter-fill').style.width = `${left}%`;
|
|
301
|
+
node.querySelector('.meter-value').textContent = `${left}% left`;
|
|
302
|
+
const when = untilTime(w.resetsAt);
|
|
303
|
+
node.title = `${w.label}: ${Math.round(w.usedPercent)}% used${when ? `, ${when}` : ''}${usage.plan ? ` (${usage.plan} plan)` : ''}`;
|
|
304
|
+
node.setAttribute('role', 'img');
|
|
305
|
+
node.setAttribute('aria-label', node.title);
|
|
306
|
+
return node;
|
|
307
|
+
}), ...creditsNote(usage));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async function loadUsage() {
|
|
311
|
+
let usage;
|
|
312
|
+
try { ({ usage } = await api('GET', '/usage')); } catch { return; }
|
|
313
|
+
state.usage = new Map(usage.map((u) => [u.providerId, u]));
|
|
314
|
+
for (const card of $('providers').children) {
|
|
315
|
+
const provider = state.providers.find((p) => p.id === card.dataset.id);
|
|
316
|
+
if (provider) renderUsage(card, provider);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const CHANNEL_LABELS = {
|
|
321
|
+
npm: 'npm', native: 'native', brew: 'Homebrew', winget: 'WinGet', legacy: 'legacy install', unknown: 'unknown install',
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
function installNote(provider) {
|
|
325
|
+
const last = provider.lastInstall;
|
|
326
|
+
if (!last) return '';
|
|
327
|
+
if (last.outcome === 'failed') {
|
|
328
|
+
const what = last.kind === 'install' ? 'Install' : 'Update';
|
|
329
|
+
return last.exitCode === null ? `${what} failed` : `${what} failed (exit ${last.exitCode})`;
|
|
330
|
+
}
|
|
331
|
+
if (last.verification === 'failed') return `Installation completed, but ${provider.tool} verification failed`;
|
|
332
|
+
if (last.outcome === 'missing') return 'Installed, but not found on PATH';
|
|
333
|
+
if (last.outcome === 'unchanged') return 'No version change after update';
|
|
334
|
+
return '';
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function providerState(provider) {
|
|
338
|
+
const note = installNote(provider);
|
|
339
|
+
if (!provider.available) return note ? `Not installed · ${note}` : 'Not installed';
|
|
340
|
+
const checkFailed = provider.versionStatus === 'failed';
|
|
341
|
+
const named = provider.installChannel && (provider.installChannel !== 'unknown' || provider.updateAvailable || checkFailed);
|
|
342
|
+
const channel = named ? CHANNEL_LABELS[provider.installChannel] || provider.installChannel : '';
|
|
343
|
+
if (checkFailed) {
|
|
344
|
+
const unverified = note && provider.lastInstall.outcome !== 'failed' && provider.lastInstall.verification === 'failed';
|
|
345
|
+
return (unverified ? [note, channel] : ['Version check failed', channel, note]).filter(Boolean).join(' · ');
|
|
346
|
+
}
|
|
347
|
+
const parts = ['Ready'];
|
|
348
|
+
if (provider.installedVersion) parts.push(`v${provider.installedVersion}`);
|
|
349
|
+
else if (provider.versionStatus === 'unavailable') parts.push('Version unavailable');
|
|
350
|
+
if (channel) parts.push(channel);
|
|
351
|
+
if (provider.updateAvailable) parts.push(`${provider.latestVersion} ${provider.installChannel === 'npm' ? 'available' : 'released'}`);
|
|
352
|
+
if (note) parts.push(note);
|
|
353
|
+
return parts.join(' · ');
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async function installProvider(provider, card, { force = false } = {}) {
|
|
357
|
+
card.classList.add('busy');
|
|
358
|
+
try {
|
|
359
|
+
const { session } = await api('POST', `/providers/${provider.id}/install`, { force });
|
|
360
|
+
upsertSession(session);
|
|
361
|
+
openPanel(session.id);
|
|
362
|
+
} catch (err) {
|
|
363
|
+
if (err instanceof AuthError) return showAuth(err.message);
|
|
364
|
+
if (err.code === 'provider_in_use') {
|
|
365
|
+
card.classList.remove('busy');
|
|
366
|
+
const n = err.running;
|
|
367
|
+
const what = `${n} ${provider.tool} session${n === 1 ? ' is' : 's are'} running`;
|
|
368
|
+
if (confirm(`${what}. Updating ${provider.tool} while it runs can break ${n === 1 ? 'that session' : 'those sessions'}. Update anyway?`)) {
|
|
369
|
+
return installProvider(provider, card, { force: true });
|
|
370
|
+
}
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
toast(err.message, 8000);
|
|
374
|
+
} finally {
|
|
375
|
+
card.classList.remove('busy');
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
async function startSession(provider, card, { resume } = {}) {
|
|
380
|
+
const cwd = $('cwd').value.trim();
|
|
381
|
+
save(CWD_KEY, cwd);
|
|
382
|
+
card.classList.add('busy');
|
|
383
|
+
try {
|
|
384
|
+
const body = { providerId: provider.id, cwd: cwd || undefined, cols: 120, rows: 32, resume };
|
|
385
|
+
const { session } = await api('POST', '/sessions', body);
|
|
386
|
+
upsertSession(session);
|
|
387
|
+
openPanel(session.id);
|
|
388
|
+
} catch (err) {
|
|
389
|
+
if (err instanceof AuthError) return showAuth(err.message);
|
|
390
|
+
toast(err.message, 8000);
|
|
391
|
+
} finally {
|
|
392
|
+
card.classList.remove('busy');
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function resumeSession(provider, card) {
|
|
397
|
+
const id = prompt(`${provider.tool} session id or name to resume`);
|
|
398
|
+
if (id === null || !id.trim()) return;
|
|
399
|
+
startSession(provider, card, { resume: id.trim() });
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// ---- session cards --------------------------------------------------------
|
|
403
|
+
|
|
404
|
+
const cards = new Map();
|
|
405
|
+
|
|
406
|
+
const MODEL_SOURCES = { report: 'reported by the tool', screen: 'seen on the tool\'s screen', args: 'from the --model argument' };
|
|
407
|
+
|
|
408
|
+
function modelText(s) {
|
|
409
|
+
return s.model ? s.model.displayName || s.model.name : '';
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function modelTitle(s) {
|
|
413
|
+
if (!s.model) return '';
|
|
414
|
+
const id = s.model.displayName && s.model.displayName !== s.model.name ? ` (${s.model.name})` : '';
|
|
415
|
+
return `Model ${modelText(s)}${id}, ${MODEL_SOURCES[s.model.source] || s.model.source}`;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function statusText(s) {
|
|
419
|
+
if (s.status === 'exited') {
|
|
420
|
+
if (s.signal) return `Exited (${s.signal})`;
|
|
421
|
+
return s.exitCode === 0 || s.exitCode === null ? 'Exited' : `Exited (${s.exitCode})`;
|
|
422
|
+
}
|
|
423
|
+
return s.activity === 'active' ? 'Working' : 'Running';
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function buildCard(session) {
|
|
427
|
+
const node = $('session-template').content.firstElementChild.cloneNode(true);
|
|
428
|
+
node.dataset.id = session.id;
|
|
429
|
+
const open = () => openPanel(session.id);
|
|
430
|
+
node.addEventListener('click', (e) => { if (!e.target.closest('button')) open(); });
|
|
431
|
+
node.addEventListener('keydown', (e) => { if (e.key === 'Enter' && e.target === node) open(); });
|
|
432
|
+
node.querySelector('.open').addEventListener('click', open);
|
|
433
|
+
node.querySelector('.stop').addEventListener('click', () => stopSession(session.id));
|
|
434
|
+
node.querySelector('.remove').addEventListener('click', () => removeSession(session.id));
|
|
435
|
+
node.querySelector('.rename').addEventListener('click', () => renameSession(session.id));
|
|
436
|
+
return node;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function sessionLevel(s) {
|
|
440
|
+
const end = s.status === 'exited'
|
|
441
|
+
? Date.parse(s.exitedAt ?? s.lastOutputAt ?? s.createdAt)
|
|
442
|
+
: Date.now();
|
|
443
|
+
const hours = (end - Date.parse(s.createdAt)) / 3_600_000;
|
|
444
|
+
return Number.isFinite(hours) ? Math.max(0, Math.floor(hours)) + 1 : 1;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function updateCard(node, s) {
|
|
448
|
+
node.dataset.provider = s.provider.id;
|
|
449
|
+
paintProviderIcon(node.querySelector('.provider-icon'), s.provider);
|
|
450
|
+
const level = sessionLevel(s);
|
|
451
|
+
const badge = node.querySelector('.level-badge');
|
|
452
|
+
badge.textContent = level;
|
|
453
|
+
badge.title = `Level ${level}`;
|
|
454
|
+
node.querySelector('.name').textContent = s.name;
|
|
455
|
+
const resumed = s.resume ? ` · resumed ${s.resume}` : '';
|
|
456
|
+
node.querySelector('.meta').textContent = `${s.provider.vendor} · ${s.provider.tool} · started ${relativeTime(s.createdAt)}${resumed}`;
|
|
457
|
+
const pill = node.querySelector('.status-pill');
|
|
458
|
+
pill.textContent = statusText(s);
|
|
459
|
+
pill.className = `status-pill ${s.status === 'exited' ? 'exited' : s.activity}`;
|
|
460
|
+
const model = node.querySelector('.model-pill');
|
|
461
|
+
model.hidden = !s.model;
|
|
462
|
+
model.textContent = modelText(s);
|
|
463
|
+
model.title = modelTitle(s);
|
|
464
|
+
model.className = `model-pill ${s.model?.source || ''}`;
|
|
465
|
+
const cwd = node.querySelector('.cwd-line');
|
|
466
|
+
// The LRM keeps a leading "/" in place under the right-to-left truncation style.
|
|
467
|
+
cwd.textContent = `\u200E${s.cwd}`;
|
|
468
|
+
cwd.title = s.cwd;
|
|
469
|
+
renderAgents(node.querySelector('.agents'), s.agents);
|
|
470
|
+
node.classList.toggle('exited', s.status === 'exited');
|
|
471
|
+
node.querySelector('.stop').hidden = s.status !== 'running';
|
|
472
|
+
node.querySelector('.remove').hidden = s.status === 'running';
|
|
473
|
+
const modelLabel = s.model ? `, model ${modelText(s)}` : '';
|
|
474
|
+
node.setAttribute('aria-label', `${s.name}, ${s.provider.vendor}${modelLabel}, ${statusText(s)}, ${s.agents.length} agents`);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function renderSessions() {
|
|
478
|
+
const grid = $('sessions');
|
|
479
|
+
const sessions = [...state.sessions.values()].sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
480
|
+
for (const [id, node] of cards) {
|
|
481
|
+
if (!state.sessions.has(id)) { node.remove(); cards.delete(id); }
|
|
482
|
+
}
|
|
483
|
+
sessions.forEach((s, index) => {
|
|
484
|
+
let node = cards.get(s.id);
|
|
485
|
+
if (!node) { node = buildCard(s); cards.set(s.id, node); }
|
|
486
|
+
updateCard(node, s);
|
|
487
|
+
// Move a card only when it is out of place: re-inserting a node drops
|
|
488
|
+
// keyboard focus and can swallow a click that is in progress.
|
|
489
|
+
if (grid.children[index] !== node) grid.insertBefore(node, grid.children[index] || null);
|
|
490
|
+
});
|
|
491
|
+
const running = sessions.filter((s) => s.status === 'running').length;
|
|
492
|
+
$('session-count').textContent = sessions.length ? `· ${running} running` : '';
|
|
493
|
+
$('empty').hidden = sessions.length > 0;
|
|
494
|
+
if (state.activeId) updatePanel();
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function upsertSession(session) {
|
|
498
|
+
state.sessions.set(session.id, session);
|
|
499
|
+
renderSessions();
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function dropSession(id) {
|
|
503
|
+
state.sessions.delete(id);
|
|
504
|
+
const view = state.views.get(id);
|
|
505
|
+
if (view) { view.dispose(); state.views.delete(id); }
|
|
506
|
+
if (state.activeId === id) closePanel();
|
|
507
|
+
renderSessions();
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
async function stopSession(id) {
|
|
511
|
+
const s = state.sessions.get(id);
|
|
512
|
+
if (!s || s.status !== 'running') return;
|
|
513
|
+
if (!confirm(`Stop "${s.name}"? The ${s.provider.tool} process will be ended.`)) return;
|
|
514
|
+
try { upsertSession((await api('POST', `/sessions/${id}/stop`)).session); } catch (err) { toast(err.message); }
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
async function removeSession(id) {
|
|
518
|
+
const s = state.sessions.get(id);
|
|
519
|
+
if (!s) return;
|
|
520
|
+
if (s.status === 'running' && !confirm(`"${s.name}" is still running. End it and remove it?`)) return;
|
|
521
|
+
try { await api('DELETE', `/sessions/${id}`); dropSession(id); } catch (err) { toast(err.message); }
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
async function renameSession(id) {
|
|
525
|
+
const s = state.sessions.get(id);
|
|
526
|
+
if (!s) return;
|
|
527
|
+
const name = prompt('Session name', s.name);
|
|
528
|
+
if (name === null || !name.trim()) return;
|
|
529
|
+
try { upsertSession((await api('PATCH', `/sessions/${id}`, { name })).session); } catch (err) { toast(err.message); }
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// ---- terminal views -------------------------------------------------------
|
|
533
|
+
|
|
534
|
+
const TERMINAL_THEME = {
|
|
535
|
+
background: '#0f1115',
|
|
536
|
+
foreground: '#e6e9ef',
|
|
537
|
+
cursor: '#e6e9ef',
|
|
538
|
+
selectionBackground: '#3a4050',
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* The session manager answers terminal queries (cursor position, device
|
|
543
|
+
* attributes, mode and colour reports) once for every session. If each
|
|
544
|
+
* attached page answered too, replies would be duplicated into the
|
|
545
|
+
* program's input. Swallow the queries here before xterm.js replies.
|
|
546
|
+
*/
|
|
547
|
+
function suppressQueryReplies(term) {
|
|
548
|
+
const swallow = () => true;
|
|
549
|
+
const csi = [
|
|
550
|
+
{ final: 'n' }, // DSR, including cursor position
|
|
551
|
+
{ prefix: '?', final: 'n' },
|
|
552
|
+
{ final: 'c' }, // primary device attributes
|
|
553
|
+
{ prefix: '>', final: 'c' }, // secondary device attributes
|
|
554
|
+
{ prefix: '=', final: 'c' }, // tertiary device attributes
|
|
555
|
+
{ intermediates: '$', final: 'p' }, // DECRQM (ANSI modes)
|
|
556
|
+
{ prefix: '?', intermediates: '$', final: 'p' }, // DECRQM (private modes)
|
|
557
|
+
{ prefix: '>', final: 'q' }, // XTVERSION
|
|
558
|
+
];
|
|
559
|
+
for (const id of csi) term.parser.registerCsiHandler(id, swallow);
|
|
560
|
+
term.parser.registerDcsHandler({ intermediates: '$', final: 'q' }, swallow); // DECRQSS
|
|
561
|
+
for (const code of [4, 10, 11, 12]) {
|
|
562
|
+
term.parser.registerOscHandler(code, (data) => data.includes('?'));
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
class TerminalView {
|
|
567
|
+
constructor(sessionId) {
|
|
568
|
+
this.id = sessionId;
|
|
569
|
+
this.el = document.createElement('div');
|
|
570
|
+
this.term = new window.Terminal({
|
|
571
|
+
cursorBlink: true,
|
|
572
|
+
fontFamily: 'ui-monospace, "Cascadia Code", "SF Mono", Menlo, Consolas, monospace',
|
|
573
|
+
fontSize: 13,
|
|
574
|
+
scrollback: 5000,
|
|
575
|
+
macOptionIsMeta: true,
|
|
576
|
+
theme: TERMINAL_THEME,
|
|
577
|
+
});
|
|
578
|
+
this.fit = new window.FitAddon.FitAddon();
|
|
579
|
+
this.term.loadAddon(this.fit);
|
|
580
|
+
this.term.loadAddon(new window.WebLinksAddon.WebLinksAddon((_e, uri) => window.open(uri, '_blank', 'noopener,noreferrer')));
|
|
581
|
+
this.term.attachCustomKeyEventHandler((e) => this.handleKey(e));
|
|
582
|
+
suppressQueryReplies(this.term);
|
|
583
|
+
this.term.onData((data) => this.send({ type: 'input', data }));
|
|
584
|
+
this.opened = false;
|
|
585
|
+
this.disposed = false;
|
|
586
|
+
this.retry = 0;
|
|
587
|
+
this.sent = { cols: 0, rows: 0 };
|
|
588
|
+
this.resizeObserver = new ResizeObserver(() => this.scheduleFit());
|
|
589
|
+
this.connect();
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
handleKey(e) {
|
|
593
|
+
if (e.type !== 'keydown') return true;
|
|
594
|
+
const key = e.key.toLowerCase();
|
|
595
|
+
// Copy: Ctrl+Shift+C, or Ctrl+C when text is selected (Windows Terminal style).
|
|
596
|
+
if (!isMac && e.ctrlKey && key === 'c' && (e.shiftKey || this.term.hasSelection())) {
|
|
597
|
+
const text = this.term.getSelection();
|
|
598
|
+
if (text) navigator.clipboard?.writeText(text).catch(() => {});
|
|
599
|
+
this.term.clearSelection();
|
|
600
|
+
e.preventDefault();
|
|
601
|
+
return false;
|
|
602
|
+
}
|
|
603
|
+
// Paste: let the browser deliver a native paste event for Ctrl+V / Ctrl+Shift+V.
|
|
604
|
+
if (!isMac && e.ctrlKey && key === 'v') return false;
|
|
605
|
+
return true;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
connect() {
|
|
609
|
+
if (this.disposed) return;
|
|
610
|
+
const ws = new WebSocket(wsUrl(`/sessions/${this.id}/terminal`));
|
|
611
|
+
this.ws = ws;
|
|
612
|
+
ws.onopen = () => { this.retry = 0; this.sent = { cols: 0, rows: 0 }; this.sendSize(); };
|
|
613
|
+
ws.onmessage = (event) => this.onMessage(JSON.parse(event.data));
|
|
614
|
+
ws.onclose = (event) => {
|
|
615
|
+
if (this.disposed || event.code === 4404 || event.code === 4410) return;
|
|
616
|
+
const delay = Math.min(5000, 300 * 2 ** this.retry++);
|
|
617
|
+
setTimeout(() => this.connect(), delay);
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
onMessage(msg) {
|
|
622
|
+
switch (msg.type) {
|
|
623
|
+
case 'snapshot':
|
|
624
|
+
this.term.reset();
|
|
625
|
+
this.term.resize(msg.cols, msg.rows);
|
|
626
|
+
this.term.write(msg.data, () => { this.sent = { cols: 0, rows: 0 }; this.refit(); });
|
|
627
|
+
break;
|
|
628
|
+
case 'data':
|
|
629
|
+
this.term.write(msg.data);
|
|
630
|
+
break;
|
|
631
|
+
case 'exit': {
|
|
632
|
+
const how = msg.signal ? `signal ${msg.signal}` : `code ${msg.exitCode ?? 0}`;
|
|
633
|
+
this.term.write(`\r\n\x1b[2m[process exited with ${how}]\x1b[0m\r\n`);
|
|
634
|
+
break;
|
|
635
|
+
}
|
|
636
|
+
default:
|
|
637
|
+
break;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
send(message) {
|
|
642
|
+
if (this.ws?.readyState === WebSocket.OPEN) this.ws.send(JSON.stringify(message));
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
sendSize() {
|
|
646
|
+
const { cols, rows } = this.term;
|
|
647
|
+
if (!this.el.isConnected || (cols === this.sent.cols && rows === this.sent.rows)) return;
|
|
648
|
+
this.sent = { cols, rows };
|
|
649
|
+
this.send({ type: 'resize', cols, rows });
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
mount(host) {
|
|
653
|
+
host.replaceChildren(this.el);
|
|
654
|
+
if (!this.opened) { this.term.open(this.el); this.opened = true; }
|
|
655
|
+
this.resizeObserver.observe(host);
|
|
656
|
+
this.refit();
|
|
657
|
+
this.term.focus();
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
unmount() {
|
|
661
|
+
this.resizeObserver.disconnect();
|
|
662
|
+
this.el.remove();
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
scheduleFit() {
|
|
666
|
+
cancelAnimationFrame(this.fitFrame);
|
|
667
|
+
this.fitFrame = requestAnimationFrame(() => this.refit());
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
refit() {
|
|
671
|
+
if (!this.el.isConnected) return;
|
|
672
|
+
try { this.fit.fit(); } catch { /* not measurable yet */ }
|
|
673
|
+
this.sendSize();
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
dispose() {
|
|
677
|
+
this.disposed = true;
|
|
678
|
+
this.resizeObserver.disconnect();
|
|
679
|
+
this.ws?.close();
|
|
680
|
+
this.term.dispose();
|
|
681
|
+
this.el.remove();
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// ---- terminal panel -------------------------------------------------------
|
|
686
|
+
|
|
687
|
+
function openPanel(id) {
|
|
688
|
+
if (!state.sessions.has(id)) return;
|
|
689
|
+
if (state.activeId && state.activeId !== id) state.views.get(state.activeId)?.unmount();
|
|
690
|
+
state.activeId = id;
|
|
691
|
+
let view = state.views.get(id);
|
|
692
|
+
if (!view) { view = new TerminalView(id); state.views.set(id, view); }
|
|
693
|
+
$('terminal-panel').hidden = false;
|
|
694
|
+
updatePanel();
|
|
695
|
+
view.mount($('terminal-host'));
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function closePanel() {
|
|
699
|
+
if (state.activeId) state.views.get(state.activeId)?.unmount();
|
|
700
|
+
state.activeId = null;
|
|
701
|
+
$('terminal-panel').hidden = true;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function updatePanel() {
|
|
705
|
+
const s = state.sessions.get(state.activeId);
|
|
706
|
+
if (!s) return;
|
|
707
|
+
paintProviderIcon($('panel-icon'), s.provider);
|
|
708
|
+
$('panel-title').textContent = s.name;
|
|
709
|
+
$('panel-sub').textContent = [s.provider.tool, modelText(s), statusText(s), s.cwd].filter(Boolean).join(' · ');
|
|
710
|
+
$('panel-sub').title = modelTitle(s);
|
|
711
|
+
renderAgents($('panel-agents'), s.agents);
|
|
712
|
+
const stop = $('panel-stop');
|
|
713
|
+
stop.textContent = s.status === 'running' ? 'Stop' : 'Remove';
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// ---- stopping the manager -------------------------------------------------
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* Stop the session manager. The manager refuses while sessions are running
|
|
720
|
+
* unless told to force, so the warning is enforced for every client and the
|
|
721
|
+
* count in the dialog is the manager's, not this page's possibly stale list.
|
|
722
|
+
*/
|
|
723
|
+
async function stopManager({ force = false } = {}) {
|
|
724
|
+
const button = $('stop-manager');
|
|
725
|
+
button.disabled = true;
|
|
726
|
+
try {
|
|
727
|
+
const { running } = await api('POST', '/shutdown', force ? { force: true } : undefined);
|
|
728
|
+
enterStopping(running);
|
|
729
|
+
} catch (err) {
|
|
730
|
+
if (err instanceof AuthError) return showAuth(err.message);
|
|
731
|
+
if (err.code === 'sessions_running') {
|
|
732
|
+
const n = err.running;
|
|
733
|
+
const what = `${n} session${n === 1 ? ' is' : 's are'} still running`;
|
|
734
|
+
const them = n === 1 ? 'it' : 'all of them';
|
|
735
|
+
if (confirm(`${what}. Stopping the session manager ends ${them}. Stop anyway?`)) {
|
|
736
|
+
return stopManager({ force: true });
|
|
737
|
+
}
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
toast(err.message, 8000);
|
|
741
|
+
} finally {
|
|
742
|
+
button.disabled = false;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/** The manager is going down, by this page's request or another client's. */
|
|
747
|
+
function enterStopping(running = 0) {
|
|
748
|
+
if (state.stopping) return;
|
|
749
|
+
state.stopping = true;
|
|
750
|
+
state.stopRemaining = null;
|
|
751
|
+
closePanel();
|
|
752
|
+
for (const view of state.views.values()) view.dispose();
|
|
753
|
+
state.views.clear();
|
|
754
|
+
state.sessions.clear();
|
|
755
|
+
renderSessions();
|
|
756
|
+
$('app').hidden = true;
|
|
757
|
+
const n = Number(running) || 0;
|
|
758
|
+
showStopped('stopping', 'Stopping the session manager…',
|
|
759
|
+
n ? `Ending ${n} running session${n === 1 ? '' : 's'}. This can take a few seconds.` : 'This can take a few seconds.');
|
|
760
|
+
setConnection('down', 'Stopping the session manager…');
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function showStopped(phase, title, text) {
|
|
764
|
+
const el = $('stopped');
|
|
765
|
+
el.classList.toggle('stopping', phase === 'stopping');
|
|
766
|
+
$('stopped-title').textContent = title;
|
|
767
|
+
$('stopped-text').textContent = text;
|
|
768
|
+
el.hidden = false;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/**
|
|
772
|
+
* The manager has stopped. Only a manager.stopped event with nothing
|
|
773
|
+
* remaining confirms that every process exited; a timeout, or a socket that
|
|
774
|
+
* dropped without the event, must not be announced as a clean stop.
|
|
775
|
+
*/
|
|
776
|
+
function showManagerStopped() {
|
|
777
|
+
setConnection('down', 'Session manager stopped');
|
|
778
|
+
const n = state.stopRemaining;
|
|
779
|
+
if (n === 0) return showStopped('stopped', 'Session manager stopped', 'Every session has ended.');
|
|
780
|
+
if (n > 0) {
|
|
781
|
+
return showStopped('stopped', 'Session manager stopped',
|
|
782
|
+
`${n} session process${n === 1 ? '' : 'es'} did not confirm exiting in time and may still be running. Check your system's process list.`);
|
|
783
|
+
}
|
|
784
|
+
showStopped('stopped', 'Session manager stopped', 'The manager went away before confirming that every session had ended.');
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// ---- events ---------------------------------------------------------------
|
|
788
|
+
|
|
789
|
+
function connectEvents() {
|
|
790
|
+
const ws = new WebSocket(wsUrl('/events'));
|
|
791
|
+
state.eventsSocket = ws;
|
|
792
|
+
ws.onopen = () => {
|
|
793
|
+
state.eventsRetry = 0;
|
|
794
|
+
setConnection('ok', 'Connected to session manager');
|
|
795
|
+
};
|
|
796
|
+
ws.onmessage = (event) => {
|
|
797
|
+
const msg = JSON.parse(event.data);
|
|
798
|
+
if (msg.type === 'hello') {
|
|
799
|
+
if (state.stopping) {
|
|
800
|
+
// The manager is back after a stop; the page picks up where it was.
|
|
801
|
+
state.stopping = false;
|
|
802
|
+
$('stopped').hidden = true;
|
|
803
|
+
$('app').hidden = false;
|
|
804
|
+
}
|
|
805
|
+
state.sessions = new Map(msg.sessions.map((s) => [s.id, s]));
|
|
806
|
+
for (const id of [...state.views.keys()]) if (!state.sessions.has(id)) dropSession(id);
|
|
807
|
+
renderSessions();
|
|
808
|
+
} else if (msg.type === 'manager.stopping') {
|
|
809
|
+
enterStopping(msg.running);
|
|
810
|
+
} else if (msg.type === 'manager.stopped') {
|
|
811
|
+
enterStopping();
|
|
812
|
+
state.stopRemaining = Number(msg.remaining) || 0;
|
|
813
|
+
showManagerStopped();
|
|
814
|
+
} else if (msg.type === 'session.created' || msg.type === 'session.updated') {
|
|
815
|
+
upsertSession(msg.session);
|
|
816
|
+
} else if (msg.type === 'session.removed') {
|
|
817
|
+
dropSession(msg.sessionId);
|
|
818
|
+
} else if (msg.type === 'providers.updated') {
|
|
819
|
+
state.providers = msg.providers;
|
|
820
|
+
renderProviders();
|
|
821
|
+
}
|
|
822
|
+
};
|
|
823
|
+
ws.onclose = () => {
|
|
824
|
+
if (state.stopping) {
|
|
825
|
+
showManagerStopped();
|
|
826
|
+
} else {
|
|
827
|
+
setConnection('down', 'Session manager not reachable. Run "agent-guild open" to start it.');
|
|
828
|
+
}
|
|
829
|
+
// Keep trying: after a stop, a relaunched manager brings the page back by itself.
|
|
830
|
+
const delay = Math.min(5000, 500 * 2 ** state.eventsRetry++);
|
|
831
|
+
setTimeout(async () => {
|
|
832
|
+
try { await loadProviders(); } catch (err) { if (err instanceof AuthError) return showAuth(err.message); }
|
|
833
|
+
connectEvents();
|
|
834
|
+
}, delay);
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
async function loadProviders() {
|
|
839
|
+
const { providers } = await api('GET', '/providers');
|
|
840
|
+
state.providers = providers;
|
|
841
|
+
renderProviders();
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// ---- auth & boot ----------------------------------------------------------
|
|
845
|
+
|
|
846
|
+
function readTokenFromHash() {
|
|
847
|
+
const params = new URLSearchParams(location.hash.slice(1));
|
|
848
|
+
const token = params.get('token');
|
|
849
|
+
if (token) {
|
|
850
|
+
// Keep the token out of the address bar and browser history.
|
|
851
|
+
history.replaceState(null, '', location.pathname + location.search);
|
|
852
|
+
}
|
|
853
|
+
return token;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
let usageTimer;
|
|
857
|
+
|
|
858
|
+
function showAuth(message = '') {
|
|
859
|
+
$('app').hidden = true;
|
|
860
|
+
$('terminal-panel').hidden = true;
|
|
861
|
+
$('stopped').hidden = true;
|
|
862
|
+
state.stopping = false;
|
|
863
|
+
$('auth').hidden = false;
|
|
864
|
+
$('auth-error').textContent = message;
|
|
865
|
+
setConnection('down', 'Not connected');
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
async function boot() {
|
|
869
|
+
$('app').hidden = true;
|
|
870
|
+
$('auth').hidden = true;
|
|
871
|
+
$('stopped').hidden = true;
|
|
872
|
+
if (!state.token) return showAuth();
|
|
873
|
+
try {
|
|
874
|
+
await loadProviders();
|
|
875
|
+
} catch (err) {
|
|
876
|
+
if (err instanceof AuthError) {
|
|
877
|
+
save(TOKEN_KEY, null);
|
|
878
|
+
return showAuth(`${err.message} Open the page again with "agent-guild open".`);
|
|
879
|
+
}
|
|
880
|
+
setConnection('down', 'Session manager not reachable. Run "agent-guild open" to start it.');
|
|
881
|
+
}
|
|
882
|
+
save(TOKEN_KEY, state.token);
|
|
883
|
+
$('app').hidden = false;
|
|
884
|
+
connectEvents();
|
|
885
|
+
loadUsage();
|
|
886
|
+
clearInterval(usageTimer);
|
|
887
|
+
usageTimer = setInterval(loadUsage, 60000);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
$('auth-form').addEventListener('submit', (e) => {
|
|
891
|
+
e.preventDefault();
|
|
892
|
+
const token = $('auth-token').value.trim();
|
|
893
|
+
if (!token) return;
|
|
894
|
+
state.token = token;
|
|
895
|
+
boot();
|
|
896
|
+
});
|
|
897
|
+
$('panel-close').addEventListener('click', closePanel);
|
|
898
|
+
$('stop-manager').addEventListener('click', () => stopManager());
|
|
899
|
+
$('panel-stop').addEventListener('click', () => {
|
|
900
|
+
const s = state.sessions.get(state.activeId);
|
|
901
|
+
if (!s) return;
|
|
902
|
+
if (s.status === 'running') stopSession(s.id);
|
|
903
|
+
else removeSession(s.id);
|
|
904
|
+
});
|
|
905
|
+
$('cwd').value = load(CWD_KEY) || '';
|
|
906
|
+
setInterval(renderSessions, 30000);
|
|
907
|
+
|
|
908
|
+
// The terminal panel sits below the top bar, which wraps onto two rows on
|
|
909
|
+
// narrow screens; publish its height so the panel never covers its controls.
|
|
910
|
+
const topbar = document.querySelector('.topbar');
|
|
911
|
+
const publishTopbarHeight = () => document.documentElement.style.setProperty('--topbar-h', `${topbar.offsetHeight}px`);
|
|
912
|
+
new ResizeObserver(publishTopbarHeight).observe(topbar);
|
|
913
|
+
publishTopbarHeight();
|
|
914
|
+
|
|
915
|
+
state.token = readTokenFromHash() || load(TOKEN_KEY);
|
|
916
|
+
boot();
|