@rexvane/piweb 0.3.1
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 +128 -0
- package/assets/showcase.png +0 -0
- package/bin/pi.js +50 -0
- package/bin/piweb.js +12 -0
- package/next.config.ts +21 -0
- package/package.json +88 -0
- package/postcss.config.mjs +7 -0
- package/public/file-icons/3d.svg +1 -0
- package/public/file-icons/LICENSE +8 -0
- package/public/file-icons/adobe-illustrator.svg +1 -0
- package/public/file-icons/adobe-photoshop.svg +1 -0
- package/public/file-icons/agent.svg +1 -0
- package/public/file-icons/asciidoc.svg +1 -0
- package/public/file-icons/assembly.svg +1 -0
- package/public/file-icons/astro.svg +1 -0
- package/public/file-icons/audio.svg +1 -0
- package/public/file-icons/authors.svg +1 -0
- package/public/file-icons/babel.svg +1 -0
- package/public/file-icons/bibliography.svg +1 -0
- package/public/file-icons/bicep.svg +1 -0
- package/public/file-icons/blender.svg +1 -0
- package/public/file-icons/bun.svg +1 -0
- package/public/file-icons/c.svg +1 -0
- package/public/file-icons/certificate.svg +1 -0
- package/public/file-icons/changelog.svg +1 -0
- package/public/file-icons/clojure.svg +1 -0
- package/public/file-icons/cmake.svg +1 -0
- package/public/file-icons/codeowners.svg +1 -0
- package/public/file-icons/commitlint.svg +1 -0
- package/public/file-icons/conduct.svg +1 -0
- package/public/file-icons/console.svg +1 -0
- package/public/file-icons/contributing.svg +1 -0
- package/public/file-icons/cpp.svg +1 -0
- package/public/file-icons/csharp.svg +1 -0
- package/public/file-icons/css.svg +1 -0
- package/public/file-icons/cursor.svg +1 -0
- package/public/file-icons/dart.svg +1 -0
- package/public/file-icons/database.svg +1 -0
- package/public/file-icons/deno.svg +1 -0
- package/public/file-icons/diff.svg +1 -0
- package/public/file-icons/docker.svg +1 -0
- package/public/file-icons/document.svg +1 -0
- package/public/file-icons/editorconfig.svg +1 -0
- package/public/file-icons/ejs.svg +1 -0
- package/public/file-icons/elixir.svg +1 -0
- package/public/file-icons/elm.svg +1 -0
- package/public/file-icons/email.svg +1 -0
- package/public/file-icons/erlang.svg +1 -0
- package/public/file-icons/eslint.svg +1 -0
- package/public/file-icons/favicon.svg +1 -0
- package/public/file-icons/file.svg +1 -0
- package/public/file-icons/folder-admin-open.svg +1 -0
- package/public/file-icons/folder-admin.svg +1 -0
- package/public/file-icons/folder-android-open.svg +1 -0
- package/public/file-icons/folder-android.svg +1 -0
- package/public/file-icons/folder-api-open.svg +1 -0
- package/public/file-icons/folder-api.svg +1 -0
- package/public/file-icons/folder-app-open.svg +1 -0
- package/public/file-icons/folder-app.svg +1 -0
- package/public/file-icons/folder-archive-open.svg +1 -0
- package/public/file-icons/folder-archive.svg +1 -0
- package/public/file-icons/folder-atom-open.svg +1 -0
- package/public/file-icons/folder-atom.svg +1 -0
- package/public/file-icons/folder-audio-open.svg +1 -0
- package/public/file-icons/folder-audio.svg +1 -0
- package/public/file-icons/folder-aws-open.svg +1 -0
- package/public/file-icons/folder-aws.svg +1 -0
- package/public/file-icons/folder-backup-open.svg +1 -0
- package/public/file-icons/folder-backup.svg +1 -0
- package/public/file-icons/folder-benchmark-open.svg +1 -0
- package/public/file-icons/folder-benchmark.svg +1 -0
- package/public/file-icons/folder-cart-open.svg +1 -0
- package/public/file-icons/folder-cart.svg +1 -0
- package/public/file-icons/folder-ci-open.svg +1 -0
- package/public/file-icons/folder-ci.svg +1 -0
- package/public/file-icons/folder-circleci-open.svg +1 -0
- package/public/file-icons/folder-circleci.svg +1 -0
- package/public/file-icons/folder-class-open.svg +1 -0
- package/public/file-icons/folder-class.svg +1 -0
- package/public/file-icons/folder-client-open.svg +1 -0
- package/public/file-icons/folder-client.svg +1 -0
- package/public/file-icons/folder-command-open.svg +1 -0
- package/public/file-icons/folder-command.svg +1 -0
- package/public/file-icons/folder-components-open.svg +1 -0
- package/public/file-icons/folder-components.svg +1 -0
- package/public/file-icons/folder-config-open.svg +1 -0
- package/public/file-icons/folder-config.svg +1 -0
- package/public/file-icons/folder-console-open.svg +1 -0
- package/public/file-icons/folder-console.svg +1 -0
- package/public/file-icons/folder-content-open.svg +1 -0
- package/public/file-icons/folder-content.svg +1 -0
- package/public/file-icons/folder-context-open.svg +1 -0
- package/public/file-icons/folder-context.svg +1 -0
- package/public/file-icons/folder-contract-open.svg +1 -0
- package/public/file-icons/folder-contract.svg +1 -0
- package/public/file-icons/folder-controller-open.svg +1 -0
- package/public/file-icons/folder-controller.svg +1 -0
- package/public/file-icons/folder-core-open.svg +1 -0
- package/public/file-icons/folder-core.svg +1 -0
- package/public/file-icons/folder-coverage-open.svg +1 -0
- package/public/file-icons/folder-coverage.svg +1 -0
- package/public/file-icons/folder-css-open.svg +1 -0
- package/public/file-icons/folder-css.svg +1 -0
- package/public/file-icons/folder-database-open.svg +1 -0
- package/public/file-icons/folder-database.svg +1 -0
- package/public/file-icons/folder-debug-open.svg +1 -0
- package/public/file-icons/folder-debug.svg +1 -0
- package/public/file-icons/folder-deprecated-open.svg +1 -0
- package/public/file-icons/folder-deprecated.svg +1 -0
- package/public/file-icons/folder-desktop-open.svg +1 -0
- package/public/file-icons/folder-desktop.svg +1 -0
- package/public/file-icons/folder-dist-open.svg +1 -0
- package/public/file-icons/folder-dist.svg +1 -0
- package/public/file-icons/folder-docker-open.svg +1 -0
- package/public/file-icons/folder-docker.svg +1 -0
- package/public/file-icons/folder-docs-open.svg +1 -0
- package/public/file-icons/folder-docs.svg +1 -0
- package/public/file-icons/folder-element-open.svg +1 -0
- package/public/file-icons/folder-element.svg +1 -0
- package/public/file-icons/folder-environment-open.svg +1 -0
- package/public/file-icons/folder-environment.svg +1 -0
- package/public/file-icons/folder-event-open.svg +1 -0
- package/public/file-icons/folder-event.svg +1 -0
- package/public/file-icons/folder-examples-open.svg +1 -0
- package/public/file-icons/folder-examples.svg +1 -0
- package/public/file-icons/folder-features-open.svg +1 -0
- package/public/file-icons/folder-features.svg +1 -0
- package/public/file-icons/folder-font-open.svg +1 -0
- package/public/file-icons/folder-font.svg +1 -0
- package/public/file-icons/folder-functions-open.svg +1 -0
- package/public/file-icons/folder-functions.svg +1 -0
- package/public/file-icons/folder-generator-open.svg +1 -0
- package/public/file-icons/folder-generator.svg +1 -0
- package/public/file-icons/folder-git-open.svg +1 -0
- package/public/file-icons/folder-git.svg +1 -0
- package/public/file-icons/folder-github-open.svg +1 -0
- package/public/file-icons/folder-github.svg +1 -0
- package/public/file-icons/folder-gitlab-open.svg +1 -0
- package/public/file-icons/folder-gitlab.svg +1 -0
- package/public/file-icons/folder-graphql-open.svg +1 -0
- package/public/file-icons/folder-graphql.svg +1 -0
- package/public/file-icons/folder-helper-open.svg +1 -0
- package/public/file-icons/folder-helper.svg +1 -0
- package/public/file-icons/folder-hook-open.svg +1 -0
- package/public/file-icons/folder-hook.svg +1 -0
- package/public/file-icons/folder-husky-open.svg +1 -0
- package/public/file-icons/folder-husky.svg +1 -0
- package/public/file-icons/folder-i18n-open.svg +1 -0
- package/public/file-icons/folder-i18n.svg +1 -0
- package/public/file-icons/folder-images-open.svg +1 -0
- package/public/file-icons/folder-images.svg +1 -0
- package/public/file-icons/folder-include-open.svg +1 -0
- package/public/file-icons/folder-include.svg +1 -0
- package/public/file-icons/folder-intellij-open.svg +1 -0
- package/public/file-icons/folder-intellij.svg +1 -0
- package/public/file-icons/folder-interface-open.svg +1 -0
- package/public/file-icons/folder-interface.svg +1 -0
- package/public/file-icons/folder-ios-open.svg +1 -0
- package/public/file-icons/folder-ios.svg +1 -0
- package/public/file-icons/folder-java-open.svg +1 -0
- package/public/file-icons/folder-java.svg +1 -0
- package/public/file-icons/folder-job-open.svg +1 -0
- package/public/file-icons/folder-job.svg +1 -0
- package/public/file-icons/folder-jupyter-open.svg +1 -0
- package/public/file-icons/folder-jupyter.svg +1 -0
- package/public/file-icons/folder-keys-open.svg +1 -0
- package/public/file-icons/folder-keys.svg +1 -0
- package/public/file-icons/folder-kubernetes-open.svg +1 -0
- package/public/file-icons/folder-kubernetes.svg +1 -0
- package/public/file-icons/folder-layout-open.svg +1 -0
- package/public/file-icons/folder-layout.svg +1 -0
- package/public/file-icons/folder-less-open.svg +1 -0
- package/public/file-icons/folder-less.svg +1 -0
- package/public/file-icons/folder-lib-open.svg +1 -0
- package/public/file-icons/folder-lib.svg +1 -0
- package/public/file-icons/folder-log-open.svg +1 -0
- package/public/file-icons/folder-log.svg +1 -0
- package/public/file-icons/folder-messages-open.svg +1 -0
- package/public/file-icons/folder-messages.svg +1 -0
- package/public/file-icons/folder-meta-open.svg +1 -0
- package/public/file-icons/folder-meta.svg +1 -0
- package/public/file-icons/folder-middleware-open.svg +1 -0
- package/public/file-icons/folder-middleware.svg +1 -0
- package/public/file-icons/folder-migrations-open.svg +1 -0
- package/public/file-icons/folder-migrations.svg +1 -0
- package/public/file-icons/folder-mobile-open.svg +1 -0
- package/public/file-icons/folder-mobile.svg +1 -0
- package/public/file-icons/folder-mock-open.svg +1 -0
- package/public/file-icons/folder-mock.svg +1 -0
- package/public/file-icons/folder-molecule-open.svg +1 -0
- package/public/file-icons/folder-molecule.svg +1 -0
- package/public/file-icons/folder-next-open.svg +1 -0
- package/public/file-icons/folder-next.svg +1 -0
- package/public/file-icons/folder-node-open.svg +1 -0
- package/public/file-icons/folder-node.svg +1 -0
- package/public/file-icons/folder-nuxt-open.svg +1 -0
- package/public/file-icons/folder-nuxt.svg +1 -0
- package/public/file-icons/folder-open.svg +1 -0
- package/public/file-icons/folder-organism-open.svg +1 -0
- package/public/file-icons/folder-organism.svg +1 -0
- package/public/file-icons/folder-packages-open.svg +1 -0
- package/public/file-icons/folder-packages.svg +1 -0
- package/public/file-icons/folder-plugin-open.svg +1 -0
- package/public/file-icons/folder-plugin.svg +1 -0
- package/public/file-icons/folder-prisma-open.svg +1 -0
- package/public/file-icons/folder-prisma.svg +1 -0
- package/public/file-icons/folder-prompts-open.svg +1 -0
- package/public/file-icons/folder-prompts.svg +1 -0
- package/public/file-icons/folder-proto-open.svg +1 -0
- package/public/file-icons/folder-proto.svg +1 -0
- package/public/file-icons/folder-public-open.svg +1 -0
- package/public/file-icons/folder-public.svg +1 -0
- package/public/file-icons/folder-python-open.svg +1 -0
- package/public/file-icons/folder-python.svg +1 -0
- package/public/file-icons/folder-queue-open.svg +1 -0
- package/public/file-icons/folder-queue.svg +1 -0
- package/public/file-icons/folder-resource-open.svg +1 -0
- package/public/file-icons/folder-resource.svg +1 -0
- package/public/file-icons/folder-review-open.svg +1 -0
- package/public/file-icons/folder-review.svg +1 -0
- package/public/file-icons/folder-robot-open.svg +1 -0
- package/public/file-icons/folder-robot.svg +1 -0
- package/public/file-icons/folder-routes-open.svg +1 -0
- package/public/file-icons/folder-routes.svg +1 -0
- package/public/file-icons/folder-rust-open.svg +1 -0
- package/public/file-icons/folder-rust.svg +1 -0
- package/public/file-icons/folder-sass-open.svg +1 -0
- package/public/file-icons/folder-sass.svg +1 -0
- package/public/file-icons/folder-scripts-open.svg +1 -0
- package/public/file-icons/folder-scripts.svg +1 -0
- package/public/file-icons/folder-secure-open.svg +1 -0
- package/public/file-icons/folder-secure.svg +1 -0
- package/public/file-icons/folder-seeders-open.svg +1 -0
- package/public/file-icons/folder-seeders.svg +1 -0
- package/public/file-icons/folder-server-open.svg +1 -0
- package/public/file-icons/folder-server.svg +1 -0
- package/public/file-icons/folder-shared-open.svg +1 -0
- package/public/file-icons/folder-shared.svg +1 -0
- package/public/file-icons/folder-skills-open.svg +1 -0
- package/public/file-icons/folder-skills.svg +1 -0
- package/public/file-icons/folder-src-open.svg +1 -0
- package/public/file-icons/folder-src.svg +1 -0
- package/public/file-icons/folder-store-open.svg +1 -0
- package/public/file-icons/folder-store.svg +1 -0
- package/public/file-icons/folder-storybook-open.svg +1 -0
- package/public/file-icons/folder-storybook.svg +1 -0
- package/public/file-icons/folder-supabase-open.svg +1 -0
- package/public/file-icons/folder-supabase.svg +1 -0
- package/public/file-icons/folder-svelte-open.svg +1 -0
- package/public/file-icons/folder-svelte.svg +1 -0
- package/public/file-icons/folder-target-open.svg +1 -0
- package/public/file-icons/folder-target.svg +1 -0
- package/public/file-icons/folder-tasks-open.svg +1 -0
- package/public/file-icons/folder-tasks.svg +1 -0
- package/public/file-icons/folder-temp-open.svg +1 -0
- package/public/file-icons/folder-temp.svg +1 -0
- package/public/file-icons/folder-template-open.svg +1 -0
- package/public/file-icons/folder-template.svg +1 -0
- package/public/file-icons/folder-terraform-open.svg +1 -0
- package/public/file-icons/folder-terraform.svg +1 -0
- package/public/file-icons/folder-test-open.svg +1 -0
- package/public/file-icons/folder-test.svg +1 -0
- package/public/file-icons/folder-theme-open.svg +1 -0
- package/public/file-icons/folder-theme.svg +1 -0
- package/public/file-icons/folder-tools-open.svg +1 -0
- package/public/file-icons/folder-tools.svg +1 -0
- package/public/file-icons/folder-turborepo-open.svg +1 -0
- package/public/file-icons/folder-turborepo.svg +1 -0
- package/public/file-icons/folder-typescript-open.svg +1 -0
- package/public/file-icons/folder-typescript.svg +1 -0
- package/public/file-icons/folder-ui-open.svg +1 -0
- package/public/file-icons/folder-ui.svg +1 -0
- package/public/file-icons/folder-utils-open.svg +1 -0
- package/public/file-icons/folder-utils.svg +1 -0
- package/public/file-icons/folder-video-open.svg +1 -0
- package/public/file-icons/folder-video.svg +1 -0
- package/public/file-icons/folder-views-open.svg +1 -0
- package/public/file-icons/folder-views.svg +1 -0
- package/public/file-icons/folder-vscode-open.svg +1 -0
- package/public/file-icons/folder-vscode.svg +1 -0
- package/public/file-icons/folder.svg +1 -0
- package/public/file-icons/font.svg +1 -0
- package/public/file-icons/fsharp.svg +1 -0
- package/public/file-icons/gemfile.svg +1 -0
- package/public/file-icons/git.svg +1 -0
- package/public/file-icons/go-mod.svg +1 -0
- package/public/file-icons/go.svg +1 -0
- package/public/file-icons/gradle.svg +1 -0
- package/public/file-icons/graphql.svg +1 -0
- package/public/file-icons/groovy.svg +1 -0
- package/public/file-icons/h.svg +1 -0
- package/public/file-icons/handlebars.svg +1 -0
- package/public/file-icons/haskell.svg +1 -0
- package/public/file-icons/hcl.svg +1 -0
- package/public/file-icons/heroku.svg +1 -0
- package/public/file-icons/hpp.svg +1 -0
- package/public/file-icons/html.svg +1 -0
- package/public/file-icons/husky.svg +1 -0
- package/public/file-icons/i18n.svg +1 -0
- package/public/file-icons/image.svg +1 -0
- package/public/file-icons/java.svg +1 -0
- package/public/file-icons/javascript.svg +1 -0
- package/public/file-icons/jenkins.svg +1 -0
- package/public/file-icons/jest.svg +1 -0
- package/public/file-icons/jsconfig.svg +1 -0
- package/public/file-icons/json.svg +1 -0
- package/public/file-icons/julia.svg +1 -0
- package/public/file-icons/jupyter.svg +1 -0
- package/public/file-icons/just.svg +1 -0
- package/public/file-icons/key.svg +1 -0
- package/public/file-icons/kotlin.svg +1 -0
- package/public/file-icons/lerna.svg +1 -0
- package/public/file-icons/less.svg +1 -0
- package/public/file-icons/license.svg +1 -0
- package/public/file-icons/lintstaged.svg +1 -0
- package/public/file-icons/liquid.svg +1 -0
- package/public/file-icons/lock.svg +1 -0
- package/public/file-icons/log.svg +1 -0
- package/public/file-icons/lua.svg +1 -0
- package/public/file-icons/makefile.svg +1 -0
- package/public/file-icons/markdown.svg +1 -0
- package/public/file-icons/maven.svg +1 -0
- package/public/file-icons/mdx.svg +1 -0
- package/public/file-icons/netlify.svg +1 -0
- package/public/file-icons/next.svg +1 -0
- package/public/file-icons/nim.svg +1 -0
- package/public/file-icons/nix.svg +1 -0
- package/public/file-icons/nodejs.svg +1 -0
- package/public/file-icons/nodemon.svg +1 -0
- package/public/file-icons/npm.svg +1 -0
- package/public/file-icons/nx.svg +1 -0
- package/public/file-icons/objective-c.svg +1 -0
- package/public/file-icons/objective-cpp.svg +1 -0
- package/public/file-icons/ocaml.svg +1 -0
- package/public/file-icons/pdf.svg +1 -0
- package/public/file-icons/php.svg +1 -0
- package/public/file-icons/pm2-ecosystem.svg +1 -0
- package/public/file-icons/pnpm.svg +1 -0
- package/public/file-icons/poetry.svg +1 -0
- package/public/file-icons/postcss.svg +1 -0
- package/public/file-icons/powerpoint.svg +1 -0
- package/public/file-icons/powershell.svg +1 -0
- package/public/file-icons/prettier.svg +1 -0
- package/public/file-icons/prisma.svg +1 -0
- package/public/file-icons/prolog.svg +1 -0
- package/public/file-icons/proto.svg +1 -0
- package/public/file-icons/pug.svg +1 -0
- package/public/file-icons/python-misc.svg +1 -0
- package/public/file-icons/python.svg +1 -0
- package/public/file-icons/r.svg +1 -0
- package/public/file-icons/razor.svg +1 -0
- package/public/file-icons/react.svg +1 -0
- package/public/file-icons/react_ts.svg +1 -0
- package/public/file-icons/readme.svg +1 -0
- package/public/file-icons/renovate.svg +1 -0
- package/public/file-icons/robots.svg +1 -0
- package/public/file-icons/rollup.svg +1 -0
- package/public/file-icons/ruby.svg +1 -0
- package/public/file-icons/rust.svg +1 -0
- package/public/file-icons/sass.svg +1 -0
- package/public/file-icons/scala.svg +1 -0
- package/public/file-icons/semantic-release.svg +1 -0
- package/public/file-icons/settings.svg +1 -0
- package/public/file-icons/shader.svg +1 -0
- package/public/file-icons/sml.svg +1 -0
- package/public/file-icons/solidity.svg +1 -0
- package/public/file-icons/stylus.svg +1 -0
- package/public/file-icons/svelte.svg +1 -0
- package/public/file-icons/svg.svg +1 -0
- package/public/file-icons/swift.svg +1 -0
- package/public/file-icons/table.svg +1 -0
- package/public/file-icons/tailwindcss.svg +1 -0
- package/public/file-icons/taskfile.svg +1 -0
- package/public/file-icons/terraform.svg +1 -0
- package/public/file-icons/test-js.svg +1 -0
- package/public/file-icons/test-jsx.svg +1 -0
- package/public/file-icons/test-ts.svg +1 -0
- package/public/file-icons/tex.svg +1 -0
- package/public/file-icons/toml.svg +1 -0
- package/public/file-icons/travis.svg +1 -0
- package/public/file-icons/tsconfig.svg +1 -0
- package/public/file-icons/tune.svg +1 -0
- package/public/file-icons/turborepo.svg +1 -0
- package/public/file-icons/typescript-def.svg +1 -0
- package/public/file-icons/typescript.svg +1 -0
- package/public/file-icons/unity.svg +1 -0
- package/public/file-icons/uv.svg +1 -0
- package/public/file-icons/vala.svg +1 -0
- package/public/file-icons/vercel.svg +1 -0
- package/public/file-icons/video.svg +1 -0
- package/public/file-icons/vim.svg +1 -0
- package/public/file-icons/vite.svg +1 -0
- package/public/file-icons/vitest.svg +1 -0
- package/public/file-icons/vlang.svg +1 -0
- package/public/file-icons/vue.svg +1 -0
- package/public/file-icons/webassembly.svg +1 -0
- package/public/file-icons/webpack.svg +1 -0
- package/public/file-icons/word.svg +1 -0
- package/public/file-icons/xml.svg +1 -0
- package/public/file-icons/yaml.svg +1 -0
- package/public/file-icons/yarn.svg +1 -0
- package/public/file-icons/zig.svg +1 -0
- package/public/file-icons/zip.svg +1 -0
- package/public/provider-icons/antgroup-color.svg +1 -0
- package/public/provider-icons/anthropic.svg +1 -0
- package/public/provider-icons/azure-color.svg +1 -0
- package/public/provider-icons/baseten.svg +1 -0
- package/public/provider-icons/bedrock-color.svg +1 -0
- package/public/provider-icons/cerebras-color.svg +1 -0
- package/public/provider-icons/cloudflare-color.svg +1 -0
- package/public/provider-icons/deepseek-color.svg +1 -0
- package/public/provider-icons/fireworks-color.svg +1 -0
- package/public/provider-icons/githubcopilot.svg +1 -0
- package/public/provider-icons/google-color.svg +1 -0
- package/public/provider-icons/googlecloud-color.svg +1 -0
- package/public/provider-icons/groq.svg +1 -0
- package/public/provider-icons/huggingface-color.svg +1 -0
- package/public/provider-icons/kimi-color.svg +1 -0
- package/public/provider-icons/minimax-color.svg +1 -0
- package/public/provider-icons/mistral-color.svg +1 -0
- package/public/provider-icons/moonshot.svg +1 -0
- package/public/provider-icons/nvidia-color.svg +1 -0
- package/public/provider-icons/openai.svg +1 -0
- package/public/provider-icons/opencode.svg +1 -0
- package/public/provider-icons/openrouter-color.svg +1 -0
- package/public/provider-icons/qwen-color.svg +1 -0
- package/public/provider-icons/together-color.svg +1 -0
- package/public/provider-icons/vercel.svg +1 -0
- package/public/provider-icons/xai.svg +1 -0
- package/public/provider-icons/xiaomimimo.svg +1 -0
- package/public/provider-icons/zai.svg +1 -0
- package/scripts/build-output.mjs +131 -0
- package/scripts/dev.mjs +18 -0
- package/scripts/entrypoint.mjs +8 -0
- package/scripts/install-build.mjs +134 -0
- package/scripts/launcher.mjs +249 -0
- package/scripts/pick-folder.ps1 +110 -0
- package/scripts/postinstall.mjs +12 -0
- package/scripts/process-runner.mjs +60 -0
- package/scripts/release.mjs +172 -0
- package/scripts/sync-file-icons.mjs +118 -0
- package/scripts/sync-provider-icons.mjs +41 -0
- package/src/app/api/agent/[id]/events/route.ts +81 -0
- package/src/app/api/agent/[id]/route.ts +24 -0
- package/src/app/api/agent/[id]/tree/route.ts +49 -0
- package/src/app/api/agent/new/route.ts +19 -0
- package/src/app/api/config/route.ts +22 -0
- package/src/app/api/files/route.ts +63 -0
- package/src/app/api/git/route.ts +32 -0
- package/src/app/api/growth/route.ts +50 -0
- package/src/app/api/health/route.ts +7 -0
- package/src/app/api/models/route.ts +129 -0
- package/src/app/api/pi-settings/route.ts +22 -0
- package/src/app/api/plugins/route.ts +79 -0
- package/src/app/api/security/route.ts +41 -0
- package/src/app/api/sessions/[id]/export/route.ts +25 -0
- package/src/app/api/sessions/[id]/route.ts +37 -0
- package/src/app/api/sessions/route.ts +23 -0
- package/src/app/api/skills/route.ts +39 -0
- package/src/app/api/update/route.ts +33 -0
- package/src/app/api/version/route.ts +8 -0
- package/src/app/api/web-auth/route.ts +80 -0
- package/src/app/api/workspaces/route.ts +78 -0
- package/src/app/globals.css +2039 -0
- package/src/app/icon.svg +21 -0
- package/src/app/layout.tsx +29 -0
- package/src/app/login/page.tsx +111 -0
- package/src/app/page.tsx +10 -0
- package/src/components/AppShell.tsx +1171 -0
- package/src/components/ChatInput.tsx +729 -0
- package/src/components/ChatWindow.tsx +1320 -0
- package/src/components/ContextMeter.tsx +131 -0
- package/src/components/DiffView.tsx +139 -0
- package/src/components/ExtensionUI.tsx +181 -0
- package/src/components/FileViewer.tsx +555 -0
- package/src/components/FullDiff.tsx +220 -0
- package/src/components/GitPanel.tsx +357 -0
- package/src/components/GrowthTree.tsx +317 -0
- package/src/components/ModelSelector.tsx +286 -0
- package/src/components/OutlineRail.tsx +166 -0
- package/src/components/PiMark.tsx +79 -0
- package/src/components/ProjectPanel.tsx +309 -0
- package/src/components/ProviderBrand.tsx +28 -0
- package/src/components/ProviderSetupModal.module.css +560 -0
- package/src/components/ProviderSetupModal.tsx +559 -0
- package/src/components/SessionSidebar.tsx +1066 -0
- package/src/components/SettingsPanel.tsx +2216 -0
- package/src/components/TrajectoryAnalyzer.module.css +225 -0
- package/src/components/TrajectoryView.tsx +289 -0
- package/src/components/icons.tsx +443 -0
- package/src/hooks/useFileViewer.ts +58 -0
- package/src/hooks/useGrowth.ts +635 -0
- package/src/hooks/usePiWeb.ts +900 -0
- package/src/i18n.tsx +1043 -0
- package/src/lib/agent-manager.ts +1348 -0
- package/src/lib/app-root.ts +4 -0
- package/src/lib/auth.ts +51 -0
- package/src/lib/command-validation.ts +116 -0
- package/src/lib/enter-behavior.ts +40 -0
- package/src/lib/extension-ui.ts +134 -0
- package/src/lib/file-icons.json +557 -0
- package/src/lib/file-icons.ts +39 -0
- package/src/lib/files-service.ts +280 -0
- package/src/lib/git-service.ts +255 -0
- package/src/lib/growth-rounds.ts +84 -0
- package/src/lib/growth-service.ts +586 -0
- package/src/lib/growth-tracker.ts +328 -0
- package/src/lib/growth-tree.ts +245 -0
- package/src/lib/growth-turns.ts +23 -0
- package/src/lib/highlight.ts +59 -0
- package/src/lib/model-draft.ts +103 -0
- package/src/lib/models-service.ts +852 -0
- package/src/lib/path-security.ts +101 -0
- package/src/lib/pi-settings.ts +115 -0
- package/src/lib/pi.ts +367 -0
- package/src/lib/plugins-service.ts +312 -0
- package/src/lib/process-format.ts +261 -0
- package/src/lib/provider-display.ts +57 -0
- package/src/lib/security-service.ts +52 -0
- package/src/lib/semver.ts +39 -0
- package/src/lib/session-reader.ts +204 -0
- package/src/lib/settings-write-lock.ts +45 -0
- package/src/lib/skills-service.ts +97 -0
- package/src/lib/text-sanitize.ts +20 -0
- package/src/lib/theme.ts +137 -0
- package/src/lib/trajectory.ts +294 -0
- package/src/lib/types.ts +300 -0
- package/src/lib/update-service.ts +129 -0
- package/src/lib/version.ts +6 -0
- package/src/lib/web-auth-shared.ts +35 -0
- package/src/lib/web-auth.ts +53 -0
- package/src/lib/workspace-store.ts +261 -0
- package/src/proxy.ts +53 -0
- package/tsconfig.json +49 -0
|
@@ -0,0 +1,1348 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-manager:AgentSession 池 + pi 事件 → Web 事件翻译 + SSE 订阅管理。
|
|
3
|
+
* 打开会话即惰性冷启动(createAgentSession),空闲回收,事件先快照后增量。
|
|
4
|
+
*/
|
|
5
|
+
import { existsSync } from "node:fs";
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
import type { AgentSession, AgentSessionEvent } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import {
|
|
9
|
+
getResourceLoader,
|
|
10
|
+
createSessionResourceLoader,
|
|
11
|
+
resourceLoaderVersion,
|
|
12
|
+
resourceLoaderIsStale,
|
|
13
|
+
getSettingsManager,
|
|
14
|
+
getAgentDir,
|
|
15
|
+
loadProjectContextFiles,
|
|
16
|
+
peekResourceLoader,
|
|
17
|
+
refreshResourceLoaderIfStale,
|
|
18
|
+
resolveProjectTrust,
|
|
19
|
+
resourceLoaderReady,
|
|
20
|
+
withResourceLock,
|
|
21
|
+
TOOL_PRESETS,
|
|
22
|
+
SessionManager,
|
|
23
|
+
createAgentSession,
|
|
24
|
+
getModelRuntime,
|
|
25
|
+
openSessionManager,
|
|
26
|
+
} from "./pi";
|
|
27
|
+
import { createExtensionUiBridge, type ExtensionUiBridge } from "./extension-ui";
|
|
28
|
+
import { createGrowthTracker, GROWTH_TRACKER_VERSION, type GrowthTracker } from "./growth-tracker";
|
|
29
|
+
import { userTurnsFromEntries } from "./growth-turns";
|
|
30
|
+
import { TrajLedger, buildTrajectoryFromEntries, toTrajTokens } from "./trajectory";
|
|
31
|
+
import { sanitizeToolOutput } from "./text-sanitize";
|
|
32
|
+
import { BoundaryError } from "./path-security";
|
|
33
|
+
import { clampThinkingLevel, getSupportedThinkingLevels } from "@earendil-works/pi-ai";
|
|
34
|
+
import type {
|
|
35
|
+
AgentCommand,
|
|
36
|
+
ContextResource,
|
|
37
|
+
GrowthStep,
|
|
38
|
+
TrajEntry,
|
|
39
|
+
ToolPreset,
|
|
40
|
+
WebEvent,
|
|
41
|
+
WebMessage,
|
|
42
|
+
WebSnapshot,
|
|
43
|
+
WebStats,
|
|
44
|
+
} from "./types";
|
|
45
|
+
|
|
46
|
+
const IDLE_MS = 10 * 60 * 1000;
|
|
47
|
+
const MAX_ACTIVE = 6;
|
|
48
|
+
const BUFFER_CAP = 400;
|
|
49
|
+
|
|
50
|
+
interface Managed {
|
|
51
|
+
sessionPath: string;
|
|
52
|
+
cwd: string;
|
|
53
|
+
sm: SessionManager;
|
|
54
|
+
session: AgentSession | null;
|
|
55
|
+
creating: Promise<AgentSession> | null;
|
|
56
|
+
subscribers: Set<(seq: number, json: string) => void>;
|
|
57
|
+
buffer: { seq: number; json: string }[];
|
|
58
|
+
seq: number;
|
|
59
|
+
ledger: TrajLedger;
|
|
60
|
+
lastActive: number;
|
|
61
|
+
toolPreset: ToolPreset;
|
|
62
|
+
/** 显式逐项选择优先于 preset;保留失效名称以便资源重新启用后恢复用户选择。 */
|
|
63
|
+
customActiveTools?: string[];
|
|
64
|
+
resourceVersion: number;
|
|
65
|
+
resourceReloadPending: boolean;
|
|
66
|
+
resourceReloading: boolean;
|
|
67
|
+
promptSubmitting: boolean;
|
|
68
|
+
runActive: boolean;
|
|
69
|
+
disposed: boolean;
|
|
70
|
+
inflightId?: string;
|
|
71
|
+
inflightIndex?: number;
|
|
72
|
+
messageIds: WeakMap<object, string>;
|
|
73
|
+
/** 用户在本会话里最后一次显式选择的思考级别;切模型后按新模型就近钳制重新应用,不让选择被 SDK 带丢 */
|
|
74
|
+
desiredThinkingLevel?: string;
|
|
75
|
+
/** 正在流式生成、尚未进入 session.messages 的助手消息(SDK 的共享 partial 对象);中途订阅的快照要带上它 */
|
|
76
|
+
inflight: unknown | null;
|
|
77
|
+
/** 扩展界面请求桥(select/confirm/input/notify → 浏览器) */
|
|
78
|
+
ui: ExtensionUiBridge | null;
|
|
79
|
+
/** 项目生长:影子仓库快照的触发器(首个会话事件时创建,dispose 时释放) */
|
|
80
|
+
growth: GrowthTracker | null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const globalForAgentManager = globalThis as typeof globalThis & {
|
|
84
|
+
__piWebSessions?: Map<string, Managed>;
|
|
85
|
+
};
|
|
86
|
+
const sessions = globalForAgentManager.__piWebSessions ??= new Map<string, Managed>();
|
|
87
|
+
|
|
88
|
+
// ---------- 消息投影 ----------
|
|
89
|
+
|
|
90
|
+
function textOf(content: any): string {
|
|
91
|
+
if (content == null) return "";
|
|
92
|
+
if (typeof content === "string") return content;
|
|
93
|
+
if (Array.isArray(content)) return content.map(textOf).join("");
|
|
94
|
+
if (typeof content === "object") {
|
|
95
|
+
if (typeof content.text === "string") return content.text;
|
|
96
|
+
if (typeof content.thinking === "string") return content.thinking;
|
|
97
|
+
}
|
|
98
|
+
return "";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function toWebMessage(m: any, entryId?: string): WebMessage {
|
|
102
|
+
if (!m || typeof m !== "object") return { role: "other", content: [] };
|
|
103
|
+
const out: WebMessage = { role: "other", content: [] };
|
|
104
|
+
if (entryId) out.id = entryId;
|
|
105
|
+
else if (typeof m.id === "string") out.id = m.id;
|
|
106
|
+
if (typeof m.timestamp === "number") out.timestamp = m.timestamp;
|
|
107
|
+
const items: WebMessage["content"] = [];
|
|
108
|
+
const content = Array.isArray(m.content) ? m.content : typeof m.content === "string" ? [{ type: "text", text: m.content }] : [];
|
|
109
|
+
for (const c of content) {
|
|
110
|
+
if (!c || typeof c !== "object") continue;
|
|
111
|
+
if (c.type === "text" && typeof c.text === "string") items.push({ type: "text", text: c.text });
|
|
112
|
+
else if (c.type === "thinking" && typeof c.thinking === "string")
|
|
113
|
+
items.push({ type: "thinking", thinking: c.thinking });
|
|
114
|
+
else if (c.type === "toolCall")
|
|
115
|
+
items.push({ type: "toolCall", id: String(c.id ?? ""), name: String(c.name ?? ""), arguments: c.arguments });
|
|
116
|
+
else if (c.type === "image" && typeof c.data === "string" && typeof c.mimeType === "string")
|
|
117
|
+
items.push({ type: "image", data: c.data, mimeType: c.mimeType });
|
|
118
|
+
}
|
|
119
|
+
out.content = items;
|
|
120
|
+
if (m.role === "user") out.role = "user";
|
|
121
|
+
else if (m.role === "assistant") {
|
|
122
|
+
out.role = "assistant";
|
|
123
|
+
out.stopReason = m.stopReason;
|
|
124
|
+
out.errorMessage = m.errorMessage ?? undefined;
|
|
125
|
+
out.usage = toTrajTokens(m.usage);
|
|
126
|
+
if (m.usage?.cost?.total != null) out.usage = { ...out.usage, cost: { total: m.usage.cost.total } };
|
|
127
|
+
out.model = typeof m.model === "string" ? m.model : undefined;
|
|
128
|
+
out.provider = typeof m.provider === "string" ? m.provider : undefined;
|
|
129
|
+
} else if (m.role === "toolResult") {
|
|
130
|
+
out.role = "toolResult";
|
|
131
|
+
const result = sanitizeToolOutput(textOf(m.content));
|
|
132
|
+
out.content = [{
|
|
133
|
+
type: "toolResult",
|
|
134
|
+
toolCallId: m.toolCallId,
|
|
135
|
+
text: result.text,
|
|
136
|
+
isError: m.isError === true,
|
|
137
|
+
encodingLoss: result.encodingLoss || undefined,
|
|
138
|
+
patch: patchOf(m.details),
|
|
139
|
+
}];
|
|
140
|
+
}
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function entryIdForMessage(sm: SessionManager, message: any): string | undefined {
|
|
145
|
+
// SDK 持久化保存同一对象引用。role+timestamp 并不唯一(同毫秒的 queued user/assistant)。
|
|
146
|
+
for (const entry of [...sm.getEntries()].reverse() as any[]) {
|
|
147
|
+
if (entry?.type === "message" && entry.message === message) return entry.id;
|
|
148
|
+
}
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function messageStreamId(m: Managed, message: unknown): string | undefined {
|
|
153
|
+
if (!message || typeof message !== "object") return undefined;
|
|
154
|
+
m.messageIds ??= new WeakMap();
|
|
155
|
+
let id = m.messageIds.get(message);
|
|
156
|
+
if (!id) {
|
|
157
|
+
id = randomUUID();
|
|
158
|
+
m.messageIds.set(message, id);
|
|
159
|
+
}
|
|
160
|
+
return id;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function projectMessage(m: Managed, message: unknown): WebMessage {
|
|
164
|
+
return { ...toWebMessage(message, entryIdForMessage(m.sm, message)), streamId: messageStreamId(m, message) };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** SDK 在 await 扩展 message_end 前先把 final 放入 messages;按 start 时的位置关联而非时间戳。 */
|
|
168
|
+
function linkInflightFinal(m: Managed, session: AgentSession): boolean {
|
|
169
|
+
if (!m.inflightId || m.inflightIndex === undefined) return false;
|
|
170
|
+
const final = session.messages[m.inflightIndex];
|
|
171
|
+
if (!final || final.role !== "assistant") return false;
|
|
172
|
+
m.messageIds.set(final, m.inflightId);
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** edit/write 工具在 details.patch 里带 unified patch;限长避免撑爆 SSE 帧 */
|
|
177
|
+
function patchOf(details: unknown): string | undefined {
|
|
178
|
+
const patch = (details as { patch?: unknown } | undefined)?.patch;
|
|
179
|
+
return typeof patch === "string" && patch.trim() ? patch.slice(0, 60_000) : undefined;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function coercePartial(p: unknown): string | undefined {
|
|
183
|
+
if (p == null) return undefined;
|
|
184
|
+
if (typeof p === "string") return p.slice(-4000);
|
|
185
|
+
if (Array.isArray(p)) return p.map(textOf).join("").slice(-4000) || undefined;
|
|
186
|
+
if (typeof p === "object") {
|
|
187
|
+
const obj = p as any;
|
|
188
|
+
const t = textOf(obj.content);
|
|
189
|
+
if (t) return t.slice(-4000);
|
|
190
|
+
try {
|
|
191
|
+
return JSON.stringify(p).slice(-4000);
|
|
192
|
+
} catch {
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ---------- 事件翻译与广播 ----------
|
|
200
|
+
|
|
201
|
+
interface QueuedPublish {
|
|
202
|
+
events: WebEvent[] | null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const publishQueues = new WeakMap<Managed, QueuedPublish[]>();
|
|
206
|
+
|
|
207
|
+
function flushPublishes(m: Managed): void {
|
|
208
|
+
const queue = publishQueues.get(m);
|
|
209
|
+
if (!queue) return;
|
|
210
|
+
while (queue.length > 0 && queue[0].events !== null) {
|
|
211
|
+
for (const evt of queue.shift()!.events!) publishImmediate(m, evt);
|
|
212
|
+
}
|
|
213
|
+
if (queue.length === 0) publishQueues.delete(m);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function publishImmediate(m: Managed, evt: WebEvent): void {
|
|
217
|
+
if (m.disposed) return;
|
|
218
|
+
m.seq += 1;
|
|
219
|
+
const json = JSON.stringify(evt);
|
|
220
|
+
m.buffer.push({ seq: m.seq, json });
|
|
221
|
+
if (m.buffer.length > BUFFER_CAP) m.buffer.shift();
|
|
222
|
+
for (const send of m.subscribers) {
|
|
223
|
+
try {
|
|
224
|
+
send(m.seq, json);
|
|
225
|
+
} catch {
|
|
226
|
+
m.subscribers.delete(send);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function publish(m: Managed, evt: WebEvent): void {
|
|
232
|
+
const queue = publishQueues.get(m);
|
|
233
|
+
if (!queue) return publishImmediate(m, evt);
|
|
234
|
+
queue.push({ events: [evt] });
|
|
235
|
+
flushPublishes(m);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function deferPublish(m: Managed, events: () => WebEvent[]): void {
|
|
239
|
+
const queue = publishQueues.get(m) ?? [];
|
|
240
|
+
const entry: QueuedPublish = { events: null };
|
|
241
|
+
queue.push(entry);
|
|
242
|
+
publishQueues.set(m, queue);
|
|
243
|
+
queueMicrotask(() => {
|
|
244
|
+
entry.events = events();
|
|
245
|
+
flushPublishes(m);
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** 模型/档位变化统一广播:同时带上新模型支持的档位,前端菜单据此刷新 */
|
|
250
|
+
function publishModelState(m: Managed, session: AgentSession): void {
|
|
251
|
+
const model: any = session.model;
|
|
252
|
+
publish(m, {
|
|
253
|
+
type: "model",
|
|
254
|
+
provider: model ? String(model.provider ?? "") : undefined,
|
|
255
|
+
model: model ? String(model.id ?? "") : undefined,
|
|
256
|
+
thinkingLevel: String(session.thinkingLevel ?? ""),
|
|
257
|
+
thinkingLevels: session.getAvailableThinkingLevels() as unknown as string[],
|
|
258
|
+
ts: Date.now(),
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** 切模型后恢复用户显式选过的级别(按新模型钳制) */
|
|
263
|
+
function reapplyDesiredLevel(m: Managed, session: AgentSession): void {
|
|
264
|
+
if (!m.desiredThinkingLevel) return;
|
|
265
|
+
const target = clampThinkingLevel(session.model as any, m.desiredThinkingLevel as any);
|
|
266
|
+
if (target !== session.thinkingLevel) session.setThinkingLevel(target as never);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function publishTraj(m: Managed): void {
|
|
270
|
+
for (const entry of m.ledger.drainDirty()) {
|
|
271
|
+
publish(m, { type: "traj", entry, ts: Date.now() });
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function timingStats(m: Managed): Pick<WebStats, "llmMs" | "toolMs" | "ttftMs" | "ttftSteps" | "decodeMs" | "decodeTokens"> {
|
|
276
|
+
let llmMs = 0;
|
|
277
|
+
let toolMs = 0;
|
|
278
|
+
let ttftMs = 0;
|
|
279
|
+
let ttftSteps = 0;
|
|
280
|
+
let decodeMs = 0;
|
|
281
|
+
let decodeTokens = 0;
|
|
282
|
+
for (const entry of m.ledger.entries) {
|
|
283
|
+
if (entry.kind === "tool") {
|
|
284
|
+
toolMs += Math.max(0, entry.timing?.durationMs ?? 0);
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
if (entry.kind !== "message") continue;
|
|
288
|
+
llmMs += Math.max(0, entry.timing?.durationMs ?? 0);
|
|
289
|
+
if (entry.timing?.ttftMs !== undefined) {
|
|
290
|
+
ttftMs += Math.max(0, entry.timing.ttftMs);
|
|
291
|
+
ttftSteps += 1;
|
|
292
|
+
}
|
|
293
|
+
if (entry.timing?.decodeMs !== undefined && entry.tokens?.output !== undefined) {
|
|
294
|
+
decodeMs += Math.max(0, entry.timing.decodeMs);
|
|
295
|
+
decodeTokens += Math.max(0, entry.tokens.output);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return { llmMs, toolMs, ttftMs, ttftSteps, decodeMs, decodeTokens };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function sessionStats(m: Managed): WebStats | null {
|
|
302
|
+
if (!m.session) return null;
|
|
303
|
+
try {
|
|
304
|
+
const stats = m.session.getSessionStats();
|
|
305
|
+
return {
|
|
306
|
+
userMessages: stats.userMessages,
|
|
307
|
+
assistantMessages: stats.assistantMessages,
|
|
308
|
+
toolCalls: stats.toolCalls,
|
|
309
|
+
totalMessages: stats.totalMessages,
|
|
310
|
+
tokens: stats.tokens,
|
|
311
|
+
cost: stats.cost,
|
|
312
|
+
...timingStats(m),
|
|
313
|
+
};
|
|
314
|
+
} catch {
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function coldSessionStats(m: Managed): WebStats {
|
|
320
|
+
let userMessages = 0;
|
|
321
|
+
let assistantMessages = 0;
|
|
322
|
+
let toolCalls = 0;
|
|
323
|
+
let totalMessages = 0;
|
|
324
|
+
const tokens = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 };
|
|
325
|
+
let cost = 0;
|
|
326
|
+
const addUsage = (usage: any) => {
|
|
327
|
+
if (!usage || typeof usage !== "object") return;
|
|
328
|
+
tokens.input += Number(usage.input ?? 0);
|
|
329
|
+
tokens.output += Number(usage.output ?? 0);
|
|
330
|
+
tokens.cacheRead += Number(usage.cacheRead ?? 0);
|
|
331
|
+
tokens.cacheWrite += Number(usage.cacheWrite ?? 0);
|
|
332
|
+
cost += Number(usage.cost?.total ?? 0);
|
|
333
|
+
};
|
|
334
|
+
for (const entry of m.sm.getEntries() as any[]) {
|
|
335
|
+
if ((entry.type === "branch_summary" || entry.type === "compaction") && entry.usage) addUsage(entry.usage);
|
|
336
|
+
if (entry.type !== "message" || !entry.message) continue;
|
|
337
|
+
totalMessages += 1;
|
|
338
|
+
const message = entry.message;
|
|
339
|
+
if (message.role === "user") userMessages += 1;
|
|
340
|
+
else if (message.role === "assistant") {
|
|
341
|
+
assistantMessages += 1;
|
|
342
|
+
toolCalls += Array.isArray(message.content)
|
|
343
|
+
? message.content.filter((content: any) => content?.type === "toolCall").length
|
|
344
|
+
: 0;
|
|
345
|
+
addUsage(message.usage);
|
|
346
|
+
} else if (message.role === "toolResult") addUsage(message.usage);
|
|
347
|
+
}
|
|
348
|
+
tokens.total = tokens.input + tokens.output + tokens.cacheRead + tokens.cacheWrite;
|
|
349
|
+
return { userMessages, assistantMessages, toolCalls, totalMessages, tokens, cost, ...timingStats(m) };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function usageEvent(m: Managed): WebEvent | null {
|
|
353
|
+
if (!m.session) return null;
|
|
354
|
+
const stats = sessionStats(m);
|
|
355
|
+
const usage = m.session.getContextUsage() ?? null;
|
|
356
|
+
return {
|
|
357
|
+
type: "usage",
|
|
358
|
+
stats,
|
|
359
|
+
contextUsage: usage ? { tokens: usage.tokens, contextWindow: usage.contextWindow, percent: usage.percent } : null,
|
|
360
|
+
ts: Date.now(),
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function publishUsage(m: Managed): void {
|
|
365
|
+
const evt = usageEvent(m);
|
|
366
|
+
if (evt) publish(m, evt);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function translate(m: Managed, evt: AgentSessionEvent): void {
|
|
370
|
+
m.ledger.onEvent(evt);
|
|
371
|
+
growthOf(m).onEvent(evt);
|
|
372
|
+
const now = Date.now();
|
|
373
|
+
switch (evt.type) {
|
|
374
|
+
case "message_start": {
|
|
375
|
+
const raw = (evt as any).message;
|
|
376
|
+
const msg = projectMessage(m, raw);
|
|
377
|
+
if (msg.role === "assistant") {
|
|
378
|
+
m.inflight = raw;
|
|
379
|
+
m.inflightId = msg.streamId;
|
|
380
|
+
m.inflightIndex = m.session?.messages.length;
|
|
381
|
+
msg.stopReason = "pending";
|
|
382
|
+
// SDK 的 partial 是被就地追加的共享对象,发到这里时首个增量往往已经写进去了;
|
|
383
|
+
// 正文一律由后续 delta 事件补齐,这里清空文本,否则浏览器会把首段拼两遍("TheThe user…")
|
|
384
|
+
msg.content = msg.content.map((c) => (c.type === "text" ? { ...c, text: "" } : c.type === "thinking" ? { ...c, thinking: "" } : c));
|
|
385
|
+
}
|
|
386
|
+
if (msg.role === "user" || msg.role === "assistant") {
|
|
387
|
+
publish(m, { type: "message", message: msg, phase: "start", ts: now });
|
|
388
|
+
}
|
|
389
|
+
break;
|
|
390
|
+
}
|
|
391
|
+
case "message_update": {
|
|
392
|
+
const e: any = (evt as any).assistantMessageEvent;
|
|
393
|
+
if ((evt as any).message) {
|
|
394
|
+
m.inflight = (evt as any).message;
|
|
395
|
+
if (m.inflightId) m.messageIds.set((evt as any).message, m.inflightId);
|
|
396
|
+
}
|
|
397
|
+
if (!e) break;
|
|
398
|
+
if (e.type === "text_delta" && typeof e.delta === "string") {
|
|
399
|
+
publish(m, { type: "delta", kind: "text", contentIndex: e.contentIndex, delta: e.delta, messageId: m.inflightId, ts: now });
|
|
400
|
+
} else if (e.type === "thinking_delta" && typeof e.delta === "string") {
|
|
401
|
+
publish(m, { type: "delta", kind: "thinking", contentIndex: e.contentIndex, delta: e.delta, messageId: m.inflightId, ts: now });
|
|
402
|
+
}
|
|
403
|
+
break;
|
|
404
|
+
}
|
|
405
|
+
case "message_end": {
|
|
406
|
+
const raw = (evt as any).message;
|
|
407
|
+
if (raw?.role === "assistant") {
|
|
408
|
+
if (m.inflightId) m.messageIds.set(raw, m.inflightId);
|
|
409
|
+
m.inflight = null;
|
|
410
|
+
m.inflightId = undefined;
|
|
411
|
+
m.inflightIndex = undefined;
|
|
412
|
+
}
|
|
413
|
+
// Pi persists message_end after notifying listeners; wait one microtask so
|
|
414
|
+
// the browser receives the durable entry ID without reordering later tool events.
|
|
415
|
+
deferPublish(m, () => {
|
|
416
|
+
const msg = projectMessage(m, raw);
|
|
417
|
+
if (msg.role === "assistant") msg.endedAt = now;
|
|
418
|
+
const usage = usageEvent(m);
|
|
419
|
+
return usage
|
|
420
|
+
? [{ type: "message", message: msg, phase: "end", ts: now }, usage]
|
|
421
|
+
: [{ type: "message", message: msg, phase: "end", ts: now }];
|
|
422
|
+
});
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
case "tool_execution_start":
|
|
426
|
+
publish(m, {
|
|
427
|
+
type: "tool",
|
|
428
|
+
id: evt.toolCallId,
|
|
429
|
+
name: evt.toolName,
|
|
430
|
+
args: evt.args,
|
|
431
|
+
state: "running",
|
|
432
|
+
ts: now,
|
|
433
|
+
});
|
|
434
|
+
break;
|
|
435
|
+
case "tool_execution_update": {
|
|
436
|
+
const partial = sanitizeToolOutput(coercePartial((evt as any).partialResult) ?? "");
|
|
437
|
+
publish(m, {
|
|
438
|
+
type: "tool",
|
|
439
|
+
id: evt.toolCallId,
|
|
440
|
+
name: evt.toolName,
|
|
441
|
+
args: (evt as any).args,
|
|
442
|
+
state: "running",
|
|
443
|
+
partialResult: partial.text || undefined,
|
|
444
|
+
encodingLoss: partial.encodingLoss || undefined,
|
|
445
|
+
ts: now,
|
|
446
|
+
});
|
|
447
|
+
break;
|
|
448
|
+
}
|
|
449
|
+
case "tool_execution_end": {
|
|
450
|
+
const result = sanitizeToolOutput(coercePartial((evt as any).result) ?? "");
|
|
451
|
+
publish(m, {
|
|
452
|
+
type: "tool",
|
|
453
|
+
id: evt.toolCallId,
|
|
454
|
+
name: evt.toolName,
|
|
455
|
+
args: (evt as any).args,
|
|
456
|
+
state: "done",
|
|
457
|
+
result: result.text || undefined,
|
|
458
|
+
isError: evt.isError,
|
|
459
|
+
encodingLoss: result.encodingLoss || undefined,
|
|
460
|
+
patch: patchOf((evt as any).result?.details),
|
|
461
|
+
ts: now,
|
|
462
|
+
});
|
|
463
|
+
publishUsage(m);
|
|
464
|
+
break;
|
|
465
|
+
}
|
|
466
|
+
case "agent_start":
|
|
467
|
+
m.runActive = true;
|
|
468
|
+
publish(m, { type: "status", isStreaming: true, state: "running", ts: now });
|
|
469
|
+
break;
|
|
470
|
+
case "agent_end":
|
|
471
|
+
// SDK 的 post-run retry/compaction/queued continuation 尚未结束。
|
|
472
|
+
publishUsage(m);
|
|
473
|
+
break;
|
|
474
|
+
case "agent_settled":
|
|
475
|
+
m.runActive = false;
|
|
476
|
+
publish(m, { type: "status", isStreaming: false, state: "idle", ts: now });
|
|
477
|
+
publishUsage(m);
|
|
478
|
+
if (m.resourceReloadPending) queueMicrotask(() => void reloadManagedResources(m));
|
|
479
|
+
break;
|
|
480
|
+
case "queue_update":
|
|
481
|
+
publish(m, {
|
|
482
|
+
type: "queue",
|
|
483
|
+
steering: [...(evt as any).steering],
|
|
484
|
+
followUp: [...(evt as any).followUp],
|
|
485
|
+
ts: now,
|
|
486
|
+
});
|
|
487
|
+
break;
|
|
488
|
+
case "compaction_start":
|
|
489
|
+
publish(m, { type: "status", isStreaming: true, state: "compacting", ts: now });
|
|
490
|
+
publish(m, { type: "compaction", phase: "start", reason: (evt as any).reason, ts: now });
|
|
491
|
+
break;
|
|
492
|
+
case "compaction_end":
|
|
493
|
+
publish(m, {
|
|
494
|
+
type: "compaction",
|
|
495
|
+
phase: "end",
|
|
496
|
+
reason: (evt as any).reason,
|
|
497
|
+
errorMessage: (evt as any).errorMessage,
|
|
498
|
+
ts: now,
|
|
499
|
+
});
|
|
500
|
+
publishUsage(m);
|
|
501
|
+
if ((evt as any).reason === "manual") {
|
|
502
|
+
publish(m, { type: "status", isStreaming: isManagedBusy(m), state: isManagedBusy(m) ? "running" : "idle", ts: now });
|
|
503
|
+
if (m.resourceReloadPending) queueMicrotask(() => void reloadManagedResources(m));
|
|
504
|
+
}
|
|
505
|
+
break;
|
|
506
|
+
case "session_info_changed":
|
|
507
|
+
publish(m, { type: "name", name: (evt as any).name ?? "", ts: now });
|
|
508
|
+
break;
|
|
509
|
+
case "thinking_level_changed":
|
|
510
|
+
if (m.session) publishModelState(m, m.session);
|
|
511
|
+
else publish(m, { type: "model", thinkingLevel: (evt as any).level, ts: now });
|
|
512
|
+
break;
|
|
513
|
+
case "auto_retry_start":
|
|
514
|
+
publish(m, { type: "error", message: `自动重试 ${(evt as any).attempt}/${(evt as any).maxAttempts}:${(evt as any).errorMessage ?? ""}`, ts: now });
|
|
515
|
+
break;
|
|
516
|
+
default:
|
|
517
|
+
break;
|
|
518
|
+
}
|
|
519
|
+
publishTraj(m);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// ---------- 项目生长 ----------
|
|
523
|
+
|
|
524
|
+
function growthOf(m: Managed): GrowthTracker {
|
|
525
|
+
if (m.growth && m.growth.version !== GROWTH_TRACKER_VERSION) {
|
|
526
|
+
m.growth.dispose();
|
|
527
|
+
m.growth = null;
|
|
528
|
+
}
|
|
529
|
+
return (m.growth ??= createGrowthTracker({ cwd: m.cwd, sessionPath: m.sessionPath, publish: (evt) => publish(m, evt) }));
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/** 手动快照(项目栏「立即快照」):记下的步同时广播给本会话的订阅者 */
|
|
533
|
+
export async function growthSnapshot(m: Managed, label = ""): Promise<GrowthStep | null> {
|
|
534
|
+
touch(m);
|
|
535
|
+
return growthOf(m).snapshotNow(label);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// ---------- 会话生命周期 ----------
|
|
539
|
+
|
|
540
|
+
function touch(m: Managed): void {
|
|
541
|
+
m.lastActive = Date.now();
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
export function getManaged(sessionPath: string): Managed {
|
|
545
|
+
let m = sessions.get(sessionPath);
|
|
546
|
+
if (!m) {
|
|
547
|
+
if (!existsSync(sessionPath)) throw new BoundaryError("session not found");
|
|
548
|
+
const sm = openSessionManager(sessionPath);
|
|
549
|
+
m = registerManaged(sessionPath, sm.getCwd() || process.cwd(), sm);
|
|
550
|
+
}
|
|
551
|
+
touch(m);
|
|
552
|
+
return m;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function registerManaged(sessionPath: string, cwd: string, sm: SessionManager): Managed {
|
|
556
|
+
let initialTrajectory: TrajEntry[] = [];
|
|
557
|
+
try {
|
|
558
|
+
initialTrajectory = buildTrajectoryFromEntries(sm.getEntries() as unknown as any[]);
|
|
559
|
+
} catch {
|
|
560
|
+
/* A damaged history can still be opened; live events start a fresh ledger. */
|
|
561
|
+
}
|
|
562
|
+
const m: Managed = {
|
|
563
|
+
sessionPath,
|
|
564
|
+
cwd,
|
|
565
|
+
sm,
|
|
566
|
+
session: null,
|
|
567
|
+
creating: null,
|
|
568
|
+
subscribers: new Set(),
|
|
569
|
+
buffer: [],
|
|
570
|
+
seq: 0,
|
|
571
|
+
inflight: null,
|
|
572
|
+
ui: null,
|
|
573
|
+
growth: null,
|
|
574
|
+
ledger: new TrajLedger(initialTrajectory),
|
|
575
|
+
lastActive: Date.now(),
|
|
576
|
+
toolPreset: "readonly",
|
|
577
|
+
resourceVersion: 0,
|
|
578
|
+
resourceReloadPending: false,
|
|
579
|
+
resourceReloading: false,
|
|
580
|
+
promptSubmitting: false,
|
|
581
|
+
runActive: false,
|
|
582
|
+
disposed: false,
|
|
583
|
+
messageIds: new WeakMap(),
|
|
584
|
+
};
|
|
585
|
+
sessions.set(sessionPath, m);
|
|
586
|
+
return m;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function loadContextResources(cwd: string, loader: AgentSession["resourceLoader"] = getResourceLoader(cwd)): ContextResource[] {
|
|
590
|
+
const files = loader.getAgentsFiles().agentsFiles.map((file) => ({
|
|
591
|
+
path: file.path,
|
|
592
|
+
content: file.content,
|
|
593
|
+
source: "project" as const,
|
|
594
|
+
}));
|
|
595
|
+
const append = loader.getAppendSystemPrompt();
|
|
596
|
+
const sources = loader.getAppendSystemPromptSources();
|
|
597
|
+
return [
|
|
598
|
+
...files,
|
|
599
|
+
...append.map((content, index) => ({
|
|
600
|
+
path: sources[index]?.path ?? `Appended system prompt ${index + 1}`,
|
|
601
|
+
content,
|
|
602
|
+
source: "extension" as const,
|
|
603
|
+
})),
|
|
604
|
+
];
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function environmentTrajectory(m: Managed, session: AgentSession | null): TrajEntry[] {
|
|
608
|
+
const ts = m.ledger.entries[0]?.ts ?? Date.now();
|
|
609
|
+
const entries: TrajEntry[] = [];
|
|
610
|
+
if (session?.systemPrompt) {
|
|
611
|
+
entries.push({
|
|
612
|
+
seq: -1,
|
|
613
|
+
kind: "system",
|
|
614
|
+
ts,
|
|
615
|
+
title: "Initial System Prompt",
|
|
616
|
+
detail: session.systemPrompt,
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
try {
|
|
621
|
+
const resources = loadContextResources(m.cwd, m.session?.resourceLoader);
|
|
622
|
+
resources.forEach((resource, index) => entries.push({
|
|
623
|
+
seq: -2 - index,
|
|
624
|
+
kind: "context",
|
|
625
|
+
ts,
|
|
626
|
+
title: resource.path,
|
|
627
|
+
detail: resource.content,
|
|
628
|
+
}));
|
|
629
|
+
} catch {
|
|
630
|
+
/* Resource discovery diagnostics are already surfaced by Pi. */
|
|
631
|
+
}
|
|
632
|
+
return entries;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function publishEnvironmentTrajectory(m: Managed, session: AgentSession): void {
|
|
636
|
+
for (const entry of environmentTrajectory(m, session)) {
|
|
637
|
+
publish(m, { type: "traj", entry, ts: Date.now() });
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
export async function ensureSession(m: Managed): Promise<AgentSession> {
|
|
642
|
+
if (m.disposed) throw new Error("session is disposed");
|
|
643
|
+
if (m.creating) return m.creating;
|
|
644
|
+
if (m.session) return m.session;
|
|
645
|
+
m.creating = (async () => {
|
|
646
|
+
await resourceLoaderReady(m.cwd);
|
|
647
|
+
const loader = await createSessionResourceLoader(m.cwd);
|
|
648
|
+
let created: AgentSession | undefined;
|
|
649
|
+
try {
|
|
650
|
+
if (m.disposed) throw new Error("session is disposed");
|
|
651
|
+
const { session } = await createAgentSession({
|
|
652
|
+
cwd: m.cwd,
|
|
653
|
+
agentDir: getAgentDir(),
|
|
654
|
+
sessionManager: m.sm,
|
|
655
|
+
modelRuntime: await getModelRuntime(),
|
|
656
|
+
resourceLoader: loader,
|
|
657
|
+
settingsManager: getSettingsManager(m.cwd),
|
|
658
|
+
});
|
|
659
|
+
created = session;
|
|
660
|
+
if (m.disposed) throw new Error("session is disposed");
|
|
661
|
+
m.session = session;
|
|
662
|
+
installSessionPolicy(m, session);
|
|
663
|
+
applyToolPolicy(m, session);
|
|
664
|
+
session.subscribe((evt) => { if (!m.disposed) translate(m, evt); });
|
|
665
|
+
const ui = createExtensionUiBridge({ publish: (evt) => publish(m, evt), hasViewers: () => !m.disposed && m.subscribers.size > 0 });
|
|
666
|
+
m.ui = ui;
|
|
667
|
+
await session.bindExtensions({
|
|
668
|
+
mode: "rpc",
|
|
669
|
+
uiContext: ui.uiContext as never,
|
|
670
|
+
onError: (error) => {
|
|
671
|
+
publish(m, { type: "error", message: `扩展 ${error.extensionPath.split(/[\\/]/).pop()} 在 ${error.event} 出错:${error.error}`, ts: Date.now() });
|
|
672
|
+
},
|
|
673
|
+
abortHandler: () => void session.abort().catch(() => undefined),
|
|
674
|
+
});
|
|
675
|
+
if (m.disposed) throw new Error("session is disposed");
|
|
676
|
+
applyToolPolicy(m, session);
|
|
677
|
+
m.resourceVersion = resourceLoaderVersion(m.cwd);
|
|
678
|
+
publishEnvironmentTrajectory(m, session);
|
|
679
|
+
publishTools(m, session);
|
|
680
|
+
publishResources(m);
|
|
681
|
+
publishUsage(m);
|
|
682
|
+
return session;
|
|
683
|
+
} catch (error) {
|
|
684
|
+
m.ui?.dispose();
|
|
685
|
+
m.ui = null;
|
|
686
|
+
created?.dispose();
|
|
687
|
+
loader.getExtensions().runtime.invalidate();
|
|
688
|
+
m.session = null;
|
|
689
|
+
throw error;
|
|
690
|
+
}
|
|
691
|
+
})().catch((err) => {
|
|
692
|
+
if (!m.disposed) publish(m, { type: "error", message: String(err?.message ?? err), ts: Date.now() });
|
|
693
|
+
throw err;
|
|
694
|
+
}).finally(() => { m.creating = null; });
|
|
695
|
+
return m.creating;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function isManagedBusy(m: Managed): boolean {
|
|
699
|
+
return m.promptSubmitting || m.resourceReloading || m.runActive || !!(m.session && !m.session.isIdle);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function allowedToolNames(m: Managed, session: AgentSession): string[] {
|
|
703
|
+
const allowed = m.customActiveTools ?? (m.toolPreset === "full" ? undefined : TOOL_PRESETS[m.toolPreset]);
|
|
704
|
+
return listAllToolNames(session).filter((name) => !allowed || allowed.includes(name));
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function applyToolPolicy(m: Managed, session: AgentSession): void {
|
|
708
|
+
session.setActiveToolsByName(allowedToolNames(m, session));
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function publishTools(m: Managed, session: AgentSession): void {
|
|
712
|
+
publish(m, { type: "tools", active: listActiveTools(session), all: listAllToolNames(session), toolPreset: m.toolPreset, customActiveTools: m.customActiveTools ?? null, ts: Date.now() });
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/** SDK reload 自动启用扩展工具,注册新工具也会刷新全集;统一在激活边界收敛到服务端选择。 */
|
|
716
|
+
function installSessionPolicy(m: Managed, session: AgentSession): void {
|
|
717
|
+
const setActive = session.setActiveToolsByName.bind(session);
|
|
718
|
+
session.setActiveToolsByName = (names) => {
|
|
719
|
+
const allowed = allowedToolNames(m, session);
|
|
720
|
+
setActive([...new Set(names)].filter((name) => allowed.includes(name)));
|
|
721
|
+
};
|
|
722
|
+
const reload = session.reload.bind(session);
|
|
723
|
+
session.reload = async (options) => {
|
|
724
|
+
if (m.disposed) throw new Error("session is disposed");
|
|
725
|
+
if (isManagedBusy(m)) throw new Error("session is busy");
|
|
726
|
+
m.resourceReloading = true;
|
|
727
|
+
try {
|
|
728
|
+
await withResourceLock(async () => {
|
|
729
|
+
if (m.disposed || m.runActive || !session.isIdle) throw new Error("session is busy");
|
|
730
|
+
await reload({ beforeSessionStart: async () => {
|
|
731
|
+
applyToolPolicy(m, session);
|
|
732
|
+
await options?.beforeSessionStart?.();
|
|
733
|
+
applyToolPolicy(m, session);
|
|
734
|
+
} });
|
|
735
|
+
});
|
|
736
|
+
m.resourceVersion = resourceLoaderVersion(m.cwd);
|
|
737
|
+
} finally {
|
|
738
|
+
applyToolPolicy(m, session);
|
|
739
|
+
m.resourceReloading = false;
|
|
740
|
+
if (!m.disposed) {
|
|
741
|
+
publishTools(m, session);
|
|
742
|
+
publishEnvironmentTrajectory(m, session);
|
|
743
|
+
publishResources(m);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function listAllToolNames(session: AgentSession): string[] {
|
|
750
|
+
const all = (session as unknown as { getAllTools?: () => Array<{ name: string }> }).getAllTools?.();
|
|
751
|
+
return all ? all.map((t) => t.name) : [];
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function listActiveTools(session: AgentSession): string[] {
|
|
755
|
+
// SDK 的真名是 getActiveToolNames(getActiveTools 不存在)
|
|
756
|
+
const active = (session as unknown as { getActiveToolNames?: () => string[] }).getActiveToolNames?.();
|
|
757
|
+
return active ?? [];
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
export function disposeSession(m: Managed): void {
|
|
761
|
+
if (m.disposed) return;
|
|
762
|
+
m.disposed = true;
|
|
763
|
+
m.ui?.dispose();
|
|
764
|
+
m.ui = null;
|
|
765
|
+
m.growth?.dispose();
|
|
766
|
+
m.growth = null;
|
|
767
|
+
try {
|
|
768
|
+
m.session?.dispose();
|
|
769
|
+
} catch {
|
|
770
|
+
/* ignore */
|
|
771
|
+
}
|
|
772
|
+
// 正在创建中的会话:创建完成后立即销毁,否则它会挂在已被移出池的
|
|
773
|
+
// Managed 上,订阅与句柄永久泄漏(同一 JSONL 还会出现双活跃会话)。
|
|
774
|
+
if (m.creating) {
|
|
775
|
+
void m.creating
|
|
776
|
+
.then((session) => {
|
|
777
|
+
try {
|
|
778
|
+
session?.dispose();
|
|
779
|
+
} catch {
|
|
780
|
+
/* ignore */
|
|
781
|
+
}
|
|
782
|
+
})
|
|
783
|
+
.catch(() => undefined);
|
|
784
|
+
}
|
|
785
|
+
for (const send of m.subscribers) {
|
|
786
|
+
try {
|
|
787
|
+
send(m.seq, JSON.stringify({ type: "status", isStreaming: false, state: "disposed", ts: Date.now() }));
|
|
788
|
+
} catch {
|
|
789
|
+
/* ignore */
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
m.subscribers.clear();
|
|
793
|
+
sessions.delete(m.sessionPath);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
export async function disposeSessionPath(sessionPath: string): Promise<void> {
|
|
797
|
+
const m = sessions.get(sessionPath);
|
|
798
|
+
if (!m) return;
|
|
799
|
+
const session = m.session ?? (m.creating ? await m.creating.catch(() => null) : null);
|
|
800
|
+
if (session?.isStreaming) await session.abort().catch(() => undefined);
|
|
801
|
+
disposeSession(m);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/** 空闲回收 + 活跃数上限(LRU) */
|
|
805
|
+
export function reap(): void {
|
|
806
|
+
const now = Date.now();
|
|
807
|
+
for (const m of [...sessions.values()]) {
|
|
808
|
+
if (m.subscribers.size > 0 || now - m.lastActive <= IDLE_MS) continue;
|
|
809
|
+
if (!m.creating && !isManagedBusy(m)) disposeSession(m);
|
|
810
|
+
}
|
|
811
|
+
const active = [...sessions.values()].filter((m) => m.session);
|
|
812
|
+
if (active.length > MAX_ACTIVE) {
|
|
813
|
+
const evictable = active
|
|
814
|
+
.filter((m) => !m.creating && !isManagedBusy(m) && m.subscribers.size === 0)
|
|
815
|
+
.sort((a, b) => a.lastActive - b.lastActive);
|
|
816
|
+
for (const m of evictable.slice(0, active.length - MAX_ACTIVE)) disposeSession(m);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
setInterval(reap, 60_000).unref?.();
|
|
821
|
+
|
|
822
|
+
// ---------- 资源清单(技能 / 提示模板 / 扩展命令 / 信任 / 诊断) ----------
|
|
823
|
+
|
|
824
|
+
type ResourceBundle = Pick<WebSnapshot, "skills" | "promptTemplates" | "extensionCommands" | "projectTrust" | "resourceDiagnostics">;
|
|
825
|
+
|
|
826
|
+
function collectResources(m: Managed): ResourceBundle {
|
|
827
|
+
const loader = m.session?.resourceLoader ?? peekResourceLoader(m.cwd) ?? getResourceLoader(m.cwd);
|
|
828
|
+
const diagnostics: WebSnapshot["resourceDiagnostics"] = [];
|
|
829
|
+
let skills: WebSnapshot["skills"] = [];
|
|
830
|
+
try {
|
|
831
|
+
const r = loader.getSkills();
|
|
832
|
+
skills = r.skills.map((sk) => ({ name: String(sk.name ?? ""), description: String(sk.description ?? "") }));
|
|
833
|
+
for (const d of r.diagnostics ?? []) diagnostics.push({ kind: "skill", path: d.path, message: d.message });
|
|
834
|
+
} catch {
|
|
835
|
+
skills = [];
|
|
836
|
+
}
|
|
837
|
+
let promptTemplates: WebSnapshot["promptTemplates"] = [];
|
|
838
|
+
try {
|
|
839
|
+
const r = loader.getPrompts();
|
|
840
|
+
promptTemplates = r.prompts.map((p) => ({ name: String(p.name ?? ""), description: String(p.description ?? ""), argumentHint: p.argumentHint }));
|
|
841
|
+
for (const d of r.diagnostics ?? []) diagnostics.push({ kind: "prompt", path: d.path, message: d.message });
|
|
842
|
+
} catch {
|
|
843
|
+
promptTemplates = [];
|
|
844
|
+
}
|
|
845
|
+
try {
|
|
846
|
+
for (const e of loader.getExtensions().errors ?? []) diagnostics.push({ kind: "extension", path: e.path, message: e.error });
|
|
847
|
+
} catch {
|
|
848
|
+
/* ignore */
|
|
849
|
+
}
|
|
850
|
+
let extensionCommands: WebSnapshot["extensionCommands"] = [];
|
|
851
|
+
if (m.session) {
|
|
852
|
+
try {
|
|
853
|
+
const runner = (m.session as unknown as { extensionRunner?: { getRegisteredCommands(): Array<{ invocationName: string; description?: string; sourceInfo?: { path?: string } }>; getCommandDiagnostics(): Array<{ path?: string; message: string }> } }).extensionRunner;
|
|
854
|
+
if (runner) {
|
|
855
|
+
extensionCommands = runner.getRegisteredCommands().map((c) => ({
|
|
856
|
+
name: c.invocationName,
|
|
857
|
+
description: String(c.description ?? ""),
|
|
858
|
+
source: String(c.sourceInfo?.path ?? "").split(/[\\/]/).pop() ?? "",
|
|
859
|
+
}));
|
|
860
|
+
for (const d of runner.getCommandDiagnostics() ?? []) diagnostics.push({ kind: "command", path: d.path, message: d.message });
|
|
861
|
+
}
|
|
862
|
+
} catch {
|
|
863
|
+
extensionCommands = [];
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
return { skills, promptTemplates, extensionCommands, projectTrust: resolveProjectTrust(m.cwd), resourceDiagnostics: diagnostics };
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
export function publishResources(m: Managed): void {
|
|
870
|
+
publish(m, { type: "resources", ...collectResources(m), ts: Date.now() });
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
/**
|
|
874
|
+
* 资源变更后(安装插件 / 新建模板 / 切换信任):让持有该 cwd 的活跃会话重建扩展运行时
|
|
875
|
+
* (SDK 的 session.reload() = pi 的 /reload),再广播新清单。正在流式的会话跳过,等它空闲。
|
|
876
|
+
*/
|
|
877
|
+
async function reloadManagedResources(m: Managed): Promise<boolean> {
|
|
878
|
+
if (m.disposed || !m.session || m.creating || isManagedBusy(m)) return false;
|
|
879
|
+
m.resourceReloadPending = false;
|
|
880
|
+
try {
|
|
881
|
+
await m.session.reload();
|
|
882
|
+
return true;
|
|
883
|
+
} catch (err) {
|
|
884
|
+
publish(m, { type: "error", message: `重载扩展失败:${String((err as Error)?.message ?? err)}`, ts: Date.now() });
|
|
885
|
+
return false;
|
|
886
|
+
} finally {
|
|
887
|
+
if (m.resourceReloadPending && !isManagedBusy(m)) queueMicrotask(() => void reloadManagedResources(m));
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
export async function reloadSessionsForCwd(cwd?: string): Promise<number> {
|
|
892
|
+
const key = (p: string) => (process.platform === "win32" ? p.replace(/\\/g, "/").toLowerCase() : p.replace(/\\/g, "/"));
|
|
893
|
+
let n = 0;
|
|
894
|
+
for (const m of sessions.values()) {
|
|
895
|
+
if (cwd && key(m.cwd) !== key(cwd)) continue;
|
|
896
|
+
if (!m.session) {
|
|
897
|
+
// 冷会话没有扩展运行时可重建,但页面可能开着:把新清单(模板 / 技能 / 信任)推过去
|
|
898
|
+
if (m.subscribers.size > 0) publishResources(m);
|
|
899
|
+
continue;
|
|
900
|
+
}
|
|
901
|
+
if (m.creating || isManagedBusy(m)) {
|
|
902
|
+
m.resourceReloadPending = true;
|
|
903
|
+
publish(m, { type: "error", message: "资源已更新,将在本轮结束后生效", ts: Date.now() });
|
|
904
|
+
publishResources(m);
|
|
905
|
+
continue;
|
|
906
|
+
}
|
|
907
|
+
if (await reloadManagedResources(m)) n += 1;
|
|
908
|
+
}
|
|
909
|
+
return n;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// ---------- 快照 ----------
|
|
913
|
+
|
|
914
|
+
export async function buildSnapshot(m: Managed): Promise<WebSnapshot> {
|
|
915
|
+
touch(m);
|
|
916
|
+
// 冷路径的 prompt/技能/上下文资源都来自加载器,先等发现完成;
|
|
917
|
+
// 用户刚在 prompts/skills/extensions 目录新建了文件的话顺手重载一次(只 stat 几个目录)
|
|
918
|
+
await resourceLoaderReady(m.cwd);
|
|
919
|
+
try {
|
|
920
|
+
// 运行中只标记变更,不让 snapshot 请求重新执行扩展工厂/切换绑定。
|
|
921
|
+
if (m.session && isManagedBusy(m)) {
|
|
922
|
+
if (resourceLoaderIsStale(m.cwd) || m.resourceVersion !== resourceLoaderVersion(m.cwd)) m.resourceReloadPending = true;
|
|
923
|
+
} else {
|
|
924
|
+
await refreshResourceLoaderIfStale(m.cwd);
|
|
925
|
+
if (m.session && (m.resourceReloadPending || m.resourceVersion !== resourceLoaderVersion(m.cwd))) {
|
|
926
|
+
m.resourceReloadPending = true;
|
|
927
|
+
await reloadManagedResources(m);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
} catch {
|
|
931
|
+
/* 重载失败不影响快照 */
|
|
932
|
+
}
|
|
933
|
+
const session = m.session;
|
|
934
|
+
let messages: WebMessage[] = [];
|
|
935
|
+
let snapSeq: number | undefined;
|
|
936
|
+
let model: WebSnapshot["model"];
|
|
937
|
+
let thinkingLevel: string | undefined;
|
|
938
|
+
let isStreaming = false;
|
|
939
|
+
let thinkingLevels: string[] = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
940
|
+
|
|
941
|
+
if (session) {
|
|
942
|
+
const hasInflightFinal = linkInflightFinal(m, session);
|
|
943
|
+
messages = session.messages.map((message) => projectMessage(m, message));
|
|
944
|
+
// final 已被 SDK push 时只投影一次;延后的 end 使用相同 streamId 继续更新权威内容。
|
|
945
|
+
if (!hasInflightFinal && m.inflight) {
|
|
946
|
+
const partial = projectMessage(m, m.inflight);
|
|
947
|
+
if (partial.role === "assistant") messages.push({ ...partial, stopReason: "pending" });
|
|
948
|
+
}
|
|
949
|
+
snapSeq = m.seq;
|
|
950
|
+
const modelObj: any = session.model;
|
|
951
|
+
if (modelObj) model = { provider: String(modelObj.provider ?? ""), id: String(modelObj.id ?? ""), name: String(modelObj.name ?? modelObj.id ?? "") };
|
|
952
|
+
thinkingLevel = String(session.thinkingLevel ?? "");
|
|
953
|
+
try {
|
|
954
|
+
thinkingLevels = session.getAvailableThinkingLevels() as unknown as string[];
|
|
955
|
+
} catch {
|
|
956
|
+
/* 回退到完整七档 */
|
|
957
|
+
}
|
|
958
|
+
isStreaming = isManagedBusy(m);
|
|
959
|
+
} else {
|
|
960
|
+
// 冷渲染:从 JSONL 还原上下文消息(不启动 agent)
|
|
961
|
+
try {
|
|
962
|
+
const entries = m.sm.buildContextEntries();
|
|
963
|
+
messages = entries
|
|
964
|
+
.filter((e: any) => e.type === "message" && e.message)
|
|
965
|
+
.map((e: any) => toWebMessage(e.message, e.id));
|
|
966
|
+
} catch {
|
|
967
|
+
messages = [];
|
|
968
|
+
}
|
|
969
|
+
try {
|
|
970
|
+
const anySm = m.sm as unknown as { buildSessionContext?: () => { model?: { provider: string; modelId: string } | null; thinkingLevel?: string } };
|
|
971
|
+
const ctx = anySm.buildSessionContext?.();
|
|
972
|
+
if (ctx?.model) model = { provider: ctx.model.provider, id: ctx.model.modelId, name: ctx.model.modelId };
|
|
973
|
+
thinkingLevel = ctx?.thinkingLevel;
|
|
974
|
+
// 冷会话也按真实模型给档位,而不是固定七档
|
|
975
|
+
if (ctx?.model) {
|
|
976
|
+
const rt = await getModelRuntime();
|
|
977
|
+
const mm = rt.getModel(ctx.model.provider, ctx.model.modelId);
|
|
978
|
+
if (mm) {
|
|
979
|
+
thinkingLevels = getSupportedThinkingLevels(mm as any);
|
|
980
|
+
if (thinkingLevel) thinkingLevel = clampThinkingLevel(mm as any, thinkingLevel as any);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
} catch {
|
|
984
|
+
/* ignore */
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
const stats: WebStats | null = session ? sessionStats(m) : coldSessionStats(m);
|
|
989
|
+
let contextUsage: WebSnapshot["contextUsage"] = null;
|
|
990
|
+
if (session) {
|
|
991
|
+
const usage = session.getContextUsage() ?? null;
|
|
992
|
+
contextUsage = usage ? { tokens: usage.tokens, contextWindow: usage.contextWindow, percent: usage.percent } : null;
|
|
993
|
+
} else if (model) {
|
|
994
|
+
// 冷会话没有 SDK 实例:用最后一轮 assistant 用量(input+cache+output ≈ 该轮后的
|
|
995
|
+
// 上下文规模)除以模型窗口估算,避免上下文计量永远显示 0% / —
|
|
996
|
+
try {
|
|
997
|
+
const rt = await getModelRuntime();
|
|
998
|
+
const modelMeta = (rt.getModels(model.provider) as any[]).find((x) => String(x.id) === model.id);
|
|
999
|
+
const window = Number(modelMeta?.contextWindow ?? 0);
|
|
1000
|
+
let lastTurnTokens = 0;
|
|
1001
|
+
// 与 SDK getContextUsage 同一条规则:最近一次压缩之后还没有新的助手回复时,上下文大小未知(不能拿压缩前的用量充数)
|
|
1002
|
+
const entries = m.sm.getEntries() as Array<{ type?: string; message?: { role?: string; stopReason?: string } }>;
|
|
1003
|
+
let compactedSinceLastAssistant = false;
|
|
1004
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
1005
|
+
const e = entries[i];
|
|
1006
|
+
if (e.type === "compaction" || e.type === "branch_summary") {
|
|
1007
|
+
compactedSinceLastAssistant = true;
|
|
1008
|
+
break;
|
|
1009
|
+
}
|
|
1010
|
+
if (e.type === "message" && e.message?.role === "assistant" && e.message.stopReason !== "error" && e.message.stopReason !== "aborted") break;
|
|
1011
|
+
}
|
|
1012
|
+
for (let i = messages.length - 1; i >= 0 && !compactedSinceLastAssistant; i--) {
|
|
1013
|
+
const msg = messages[i];
|
|
1014
|
+
if (msg.role === "assistant" && msg.usage) {
|
|
1015
|
+
lastTurnTokens =
|
|
1016
|
+
Number(msg.usage.input ?? 0) +
|
|
1017
|
+
Number(msg.usage.cacheRead ?? 0) +
|
|
1018
|
+
Number(msg.usage.cacheWrite ?? 0) +
|
|
1019
|
+
Number(msg.usage.output ?? 0);
|
|
1020
|
+
break;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
if (window > 0 && lastTurnTokens > 0) {
|
|
1024
|
+
contextUsage = { tokens: lastTurnTokens, contextWindow: window, percent: (lastTurnTokens / window) * 100 };
|
|
1025
|
+
} else if (window > 0) {
|
|
1026
|
+
contextUsage = { tokens: null, contextWindow: window, percent: null };
|
|
1027
|
+
}
|
|
1028
|
+
} catch {
|
|
1029
|
+
/* 模型目录不可用则维持 null(UI 显示 0%) */
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
const tools = session
|
|
1034
|
+
? { active: listActiveTools(session), all: listAllToolNames(session).map((n) => ({ name: n })) }
|
|
1035
|
+
: { active: [], all: [] };
|
|
1036
|
+
const queue = session
|
|
1037
|
+
? { steering: [...session.getSteeringMessages()], followUp: [...session.getFollowUpMessages()] }
|
|
1038
|
+
: { steering: [], followUp: [] };
|
|
1039
|
+
|
|
1040
|
+
let name = "";
|
|
1041
|
+
try {
|
|
1042
|
+
name = (m.sm as unknown as { getSessionName?: () => string }).getSessionName?.() ?? "";
|
|
1043
|
+
} catch {
|
|
1044
|
+
name = "";
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// 上下文注入资源(AGENTS.md 与扩展附加 prompt)
|
|
1048
|
+
let contextResources: ContextResource[] = [];
|
|
1049
|
+
try {
|
|
1050
|
+
contextResources = loadContextResources(m.cwd, m.session?.resourceLoader);
|
|
1051
|
+
} catch {
|
|
1052
|
+
contextResources = [];
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// 斜杠命令数据源:技能 / 提示模板 / 扩展命令,加上项目信任与资源诊断
|
|
1056
|
+
const resources = collectResources(m);
|
|
1057
|
+
|
|
1058
|
+
const baseTrajectory = m.ledger.entries.length ? m.ledger.entries : coldTrajectory(m);
|
|
1059
|
+
const trajectory = [...environmentTrajectory(m, session), ...baseTrajectory];
|
|
1060
|
+
|
|
1061
|
+
return {
|
|
1062
|
+
seq: snapSeq ?? m.seq,
|
|
1063
|
+
sessionPath: m.sessionPath,
|
|
1064
|
+
cwd: m.cwd,
|
|
1065
|
+
name,
|
|
1066
|
+
contextFiles: contextResources.map((resource) => resource.path),
|
|
1067
|
+
contextResources,
|
|
1068
|
+
...resources,
|
|
1069
|
+
messages,
|
|
1070
|
+
userTurns: userTurnsFromEntries(m.sm.getEntries() as any[]),
|
|
1071
|
+
growthError: m.growth?.getError?.() ?? null,
|
|
1072
|
+
model,
|
|
1073
|
+
thinkingLevel,
|
|
1074
|
+
thinkingLevels,
|
|
1075
|
+
toolPreset: m.toolPreset,
|
|
1076
|
+
customActiveTools: m.customActiveTools ?? null,
|
|
1077
|
+
extensionUiRequests: m.ui?.getPendingRequests() ?? [],
|
|
1078
|
+
tools,
|
|
1079
|
+
stats,
|
|
1080
|
+
contextUsage,
|
|
1081
|
+
queue,
|
|
1082
|
+
trajectory,
|
|
1083
|
+
isStreaming,
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
function coldTrajectory(m: Managed): TrajEntry[] {
|
|
1088
|
+
try {
|
|
1089
|
+
return buildTrajectoryFromEntries(m.sm.getEntries() as unknown as any[]);
|
|
1090
|
+
} catch {
|
|
1091
|
+
return [];
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
// ---------- SSE 订阅 ----------
|
|
1096
|
+
|
|
1097
|
+
export function subscribe(
|
|
1098
|
+
m: Managed,
|
|
1099
|
+
send: (seq: number, json: string) => void,
|
|
1100
|
+
lastEventId: number,
|
|
1101
|
+
): boolean {
|
|
1102
|
+
touch(m);
|
|
1103
|
+
if (lastEventId < 0 || lastEventId > m.seq) return false;
|
|
1104
|
+
const replay = m.buffer.filter((b) => b.seq > lastEventId);
|
|
1105
|
+
if (replay.length !== m.seq - lastEventId) return false;
|
|
1106
|
+
const pending = new Set(m.ui?.getPendingRequests().map((request) => request.id));
|
|
1107
|
+
if (replay.some((frame) => {
|
|
1108
|
+
const event = JSON.parse(frame.json) as WebEvent;
|
|
1109
|
+
return event.type === "extension_ui" && ["select", "confirm", "input"].includes(event.method) && !pending.has(event.id);
|
|
1110
|
+
})) return false;
|
|
1111
|
+
m.subscribers.add(send);
|
|
1112
|
+
for (const b of replay) send(b.seq, b.json);
|
|
1113
|
+
return true;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
export function unsubscribe(m: Managed, send: (seq: number, json: string) => void): void {
|
|
1117
|
+
m.subscribers.delete(send);
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
// ---------- 命令执行 ----------
|
|
1121
|
+
|
|
1122
|
+
export interface CommandResult {
|
|
1123
|
+
ok: boolean;
|
|
1124
|
+
error?: string;
|
|
1125
|
+
data?: Record<string, unknown>;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
export async function execute(m: Managed, cmd: AgentCommand): Promise<CommandResult> {
|
|
1129
|
+
touch(m);
|
|
1130
|
+
try {
|
|
1131
|
+
switch (cmd.cmd) {
|
|
1132
|
+
case "prepare":
|
|
1133
|
+
await ensureSession(m);
|
|
1134
|
+
return { ok: true };
|
|
1135
|
+
case "prompt": {
|
|
1136
|
+
const text = (cmd.text ?? "").trim();
|
|
1137
|
+
if (!text && !cmd.images?.length) return { ok: false, error: "empty prompt" };
|
|
1138
|
+
// 在第一个 await 前占位,两个并发请求不能都看见 idle 后启动/误排队。
|
|
1139
|
+
if (m.promptSubmitting || m.resourceReloading) return { ok: false, error: "session is busy accepting another command" };
|
|
1140
|
+
m.promptSubmitting = true;
|
|
1141
|
+
try {
|
|
1142
|
+
const session = await ensureSession(m);
|
|
1143
|
+
if (m.disposed) throw new Error("session is disposed");
|
|
1144
|
+
// 只有用户显式选择 steer/followUp 才排队;旧的普通提交不能悄悄成为 steering。
|
|
1145
|
+
if ((session.isStreaming || m.runActive) && !cmd.behavior) return { ok: false, error: "session is busy; choose steer or followUp" };
|
|
1146
|
+
if (!session.isStreaming) await growthOf(m).prepare();
|
|
1147
|
+
if (m.disposed) throw new Error("session is disposed");
|
|
1148
|
+
return await new Promise<CommandResult>((resolve) => {
|
|
1149
|
+
let accepted = false;
|
|
1150
|
+
const run = session.prompt(text, {
|
|
1151
|
+
images: cmd.images,
|
|
1152
|
+
streamingBehavior: cmd.behavior,
|
|
1153
|
+
source: "rpc",
|
|
1154
|
+
preflightResult: (success) => {
|
|
1155
|
+
if (success) {
|
|
1156
|
+
accepted = true;
|
|
1157
|
+
resolve({ ok: true, data: { accepted: true } });
|
|
1158
|
+
}
|
|
1159
|
+
},
|
|
1160
|
+
});
|
|
1161
|
+
// 同步挂好 rejection handler;接受后的错误仍经 SSE 可见,不能成为未处理拒绝。
|
|
1162
|
+
void run.then(() => {
|
|
1163
|
+
if (!accepted) resolve({ ok: false, error: "prompt completed without acceptance" });
|
|
1164
|
+
}, (error) => {
|
|
1165
|
+
const message = String(error?.message ?? error);
|
|
1166
|
+
if (accepted) publish(m, { type: "error", message, ts: Date.now() });
|
|
1167
|
+
else resolve({ ok: false, error: message });
|
|
1168
|
+
});
|
|
1169
|
+
});
|
|
1170
|
+
} finally {
|
|
1171
|
+
m.promptSubmitting = false;
|
|
1172
|
+
if (!m.disposed) publish(m, { type: "status", isStreaming: isManagedBusy(m), state: isManagedBusy(m) ? "running" : "idle", ts: Date.now() });
|
|
1173
|
+
if (m.resourceReloadPending) queueMicrotask(() => void reloadManagedResources(m));
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
case "steer":
|
|
1177
|
+
case "followUp": {
|
|
1178
|
+
if (m.promptSubmitting || m.resourceReloading) return { ok: false, error: "session is busy accepting another command" };
|
|
1179
|
+
const session = await ensureSession(m);
|
|
1180
|
+
if (m.promptSubmitting || m.resourceReloading || !session.isStreaming) return { ok: false, error: "session is not accepting queued messages" };
|
|
1181
|
+
await session[cmd.cmd](cmd.text ?? "", cmd.images);
|
|
1182
|
+
return { ok: true };
|
|
1183
|
+
}
|
|
1184
|
+
case "clearQueue": {
|
|
1185
|
+
if (!m.session) return { ok: true, data: { steering: [], followUp: [] } };
|
|
1186
|
+
const cleared = m.session.clearQueue();
|
|
1187
|
+
publish(m, { type: "queue", steering: [], followUp: [], ts: Date.now() });
|
|
1188
|
+
return { ok: true, data: cleared };
|
|
1189
|
+
}
|
|
1190
|
+
case "abort": {
|
|
1191
|
+
if (!m.session) return { ok: true, data: { steering: [], followUp: [] } };
|
|
1192
|
+
// 先清队列再中止(pi 终端 Esc 同款):Agent.abort() 不清队列,而 agent 循环每次开跑都先取走 steering 队列,
|
|
1193
|
+
// 不清的话「已取消」的排队消息会在下一次提问时冒出来。清出来的文本交给前端放回输入框
|
|
1194
|
+
const cleared = m.session.clearQueue();
|
|
1195
|
+
await m.session.abort();
|
|
1196
|
+
publish(m, { type: "status", isStreaming: false, state: "aborted", ts: Date.now() });
|
|
1197
|
+
return { ok: true, data: cleared };
|
|
1198
|
+
}
|
|
1199
|
+
case "compact": {
|
|
1200
|
+
const session = await ensureSession(m);
|
|
1201
|
+
await session.compact(cmd.instructions || undefined);
|
|
1202
|
+
return { ok: true };
|
|
1203
|
+
}
|
|
1204
|
+
case "setModel": {
|
|
1205
|
+
const session = await ensureSession(m);
|
|
1206
|
+
const rt = await getModelRuntime();
|
|
1207
|
+
const model = rt.getModels(cmd.provider).find((x) => x.id === cmd.modelId) as any;
|
|
1208
|
+
if (!model) return { ok: false, error: `unknown model ${cmd.provider}/${cmd.modelId}` };
|
|
1209
|
+
await session.setModel(model, { persist: false });
|
|
1210
|
+
reapplyDesiredLevel(m, session);
|
|
1211
|
+
publishModelState(m, session);
|
|
1212
|
+
return { ok: true, data: { thinkingLevel: session.thinkingLevel, thinkingLevels: session.getAvailableThinkingLevels() } };
|
|
1213
|
+
}
|
|
1214
|
+
case "setThinkingLevel": {
|
|
1215
|
+
const session = await ensureSession(m);
|
|
1216
|
+
const requested = String(cmd.level ?? "medium");
|
|
1217
|
+
// 与 pi 终端一致:不支持的档位就近钳制而不是拒绝(非推理模型 → off),并把实际生效值回传
|
|
1218
|
+
const effective = clampThinkingLevel(session.model as any, requested as any);
|
|
1219
|
+
m.desiredThinkingLevel = requested;
|
|
1220
|
+
session.setThinkingLevel(effective as never);
|
|
1221
|
+
publishModelState(m, session);
|
|
1222
|
+
return { ok: true, data: { requested, thinkingLevel: session.thinkingLevel, clamped: session.thinkingLevel !== requested } };
|
|
1223
|
+
}
|
|
1224
|
+
case "setToolPreset": {
|
|
1225
|
+
const preset = cmd.preset as ToolPreset;
|
|
1226
|
+
if (!Object.hasOwn(TOOL_PRESETS, preset)) return { ok: false, error: "invalid tool preset" };
|
|
1227
|
+
if (isManagedBusy(m)) return { ok: false, error: "cannot change tools while the agent is running" };
|
|
1228
|
+
const session = await ensureSession(m);
|
|
1229
|
+
if (isManagedBusy(m)) return { ok: false, error: "cannot change tools while the agent is running" };
|
|
1230
|
+
m.toolPreset = preset;
|
|
1231
|
+
m.customActiveTools = undefined;
|
|
1232
|
+
applyToolPolicy(m, session);
|
|
1233
|
+
publishEnvironmentTrajectory(m, session);
|
|
1234
|
+
publishTools(m, session);
|
|
1235
|
+
return { ok: true };
|
|
1236
|
+
}
|
|
1237
|
+
case "setActiveTools": {
|
|
1238
|
+
if (isManagedBusy(m)) return { ok: false, error: "cannot change tools while the agent is running" };
|
|
1239
|
+
const session = await ensureSession(m);
|
|
1240
|
+
if (isManagedBusy(m)) return { ok: false, error: "cannot change tools while the agent is running" };
|
|
1241
|
+
const wanted = Array.isArray(cmd.names) ? cmd.names.map(String) : [];
|
|
1242
|
+
const all = listAllToolNames(session);
|
|
1243
|
+
const next = [...new Set(wanted)].filter((n) => all.includes(n));
|
|
1244
|
+
if (next.length === 0) return { ok: false, error: "cannot disable all tools" };
|
|
1245
|
+
m.customActiveTools = next;
|
|
1246
|
+
applyToolPolicy(m, session);
|
|
1247
|
+
publishEnvironmentTrajectory(m, session);
|
|
1248
|
+
publishTools(m, session);
|
|
1249
|
+
return { ok: true, data: { active: listActiveTools(session) } };
|
|
1250
|
+
}
|
|
1251
|
+
case "cycleModel": {
|
|
1252
|
+
const session = await ensureSession(m);
|
|
1253
|
+
const r = await session.cycleModel(cmd.direction ?? "forward");
|
|
1254
|
+
if (r?.model) {
|
|
1255
|
+
reapplyDesiredLevel(m, session);
|
|
1256
|
+
publishModelState(m, session);
|
|
1257
|
+
return { ok: true, data: { model: String((r.model as any).id ?? ""), thinkingLevel: session.thinkingLevel } };
|
|
1258
|
+
}
|
|
1259
|
+
return { ok: false, error: "no scoped models to cycle" };
|
|
1260
|
+
}
|
|
1261
|
+
case "navigate": {
|
|
1262
|
+
const session = await ensureSession(m);
|
|
1263
|
+
const entryId = String((cmd as any).entryId ?? "");
|
|
1264
|
+
if (!entryId) return { ok: false, error: "missing entryId" };
|
|
1265
|
+
const r = await session.navigateTree(entryId);
|
|
1266
|
+
return { ok: !r?.cancelled, data: { cancelled: r?.cancelled === true, editorText: r?.editorText } };
|
|
1267
|
+
}
|
|
1268
|
+
case "rename": {
|
|
1269
|
+
const name = (cmd.text ?? "").trim();
|
|
1270
|
+
if (m.session) m.session.setSessionName(name);
|
|
1271
|
+
else (m.sm as unknown as { appendSessionInfo: (n: string) => string }).appendSessionInfo(name);
|
|
1272
|
+
publish(m, { type: "name", name, ts: Date.now() });
|
|
1273
|
+
return { ok: true };
|
|
1274
|
+
}
|
|
1275
|
+
case "extensionUiResponse": {
|
|
1276
|
+
const ok = m.ui?.respond(String(cmd.requestId), { value: cmd.value, confirmed: cmd.confirmed, cancelled: cmd.cancelled }) ?? false;
|
|
1277
|
+
return ok ? { ok: true } : { ok: false, error: "no pending extension request" };
|
|
1278
|
+
}
|
|
1279
|
+
case "reload": {
|
|
1280
|
+
if (isManagedBusy(m)) return { ok: false, error: "session is busy" };
|
|
1281
|
+
const session = await ensureSession(m);
|
|
1282
|
+
await session.reload();
|
|
1283
|
+
return { ok: true };
|
|
1284
|
+
}
|
|
1285
|
+
case "fork": {
|
|
1286
|
+
const forked = cmd.entryId
|
|
1287
|
+
? SessionManager.open(m.sessionPath).createBranchedSession(cmd.entryId)
|
|
1288
|
+
: SessionManager.forkFrom(m.sessionPath, m.cwd).getSessionFile();
|
|
1289
|
+
const newPath = forked ?? "";
|
|
1290
|
+
if (!newPath) return { ok: false, error: "failed to create forked session" };
|
|
1291
|
+
return { ok: true, data: { sessionPath: newPath } };
|
|
1292
|
+
}
|
|
1293
|
+
default:
|
|
1294
|
+
return { ok: false, error: `unknown cmd` };
|
|
1295
|
+
}
|
|
1296
|
+
} catch (err: any) {
|
|
1297
|
+
return { ok: false, error: String(err?.message ?? err) };
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
/** 新建会话(不启动 agent;发第一条消息时才冷启动) */
|
|
1302
|
+
export async function createNewSession(cwd: string): Promise<{ sessionPath: string }> {
|
|
1303
|
+
const sm = SessionManager.create(cwd);
|
|
1304
|
+
const p = sm.getSessionFile() ?? "";
|
|
1305
|
+
if (!p) throw new Error("failed to create session file");
|
|
1306
|
+
// 必须直接注册 create 得到的实例:JSONL 落盘前 SessionManager.open(p) 找不到
|
|
1307
|
+
// 文件会走新建分支,cwd 回退 process.cwd(),把会话偷换到服务器进程目录。
|
|
1308
|
+
registerManaged(p, cwd, sm);
|
|
1309
|
+
return { sessionPath: p };
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
/** 活跃会话状态(给侧栏状态点用) */
|
|
1313
|
+
export function activeStatus(): Record<string, { streaming: boolean; lastStep?: string }> {
|
|
1314
|
+
const out: Record<string, { streaming: boolean; lastStep?: string }> = {};
|
|
1315
|
+
for (const [p, m] of sessions) {
|
|
1316
|
+
if (!m.session) continue;
|
|
1317
|
+
// 侧栏显示“最后一步”:正在跑的会话让用户一眼知道它在干什么
|
|
1318
|
+
let lastStep: string | undefined;
|
|
1319
|
+
for (let i = m.ledger.entries.length - 1; i >= 0; i -= 1) {
|
|
1320
|
+
const e = m.ledger.entries[i];
|
|
1321
|
+
if (e.kind === "tool") {
|
|
1322
|
+
lastStep = [e.toolName, (e.preview ?? e.title ?? "").split("\n")[0].slice(0, 80)].filter(Boolean).join(" · ");
|
|
1323
|
+
break;
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
out[p] = { streaming: isManagedBusy(m), lastStep };
|
|
1327
|
+
}
|
|
1328
|
+
return out;
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
/** 导出(html 需要真实 AgentSession,冷会话会静默冷启动) */
|
|
1332
|
+
export async function exportSession(
|
|
1333
|
+
m: Managed,
|
|
1334
|
+
format: "jsonl" | "html",
|
|
1335
|
+
): Promise<{ filename: string; content: string; contentType: string }> {
|
|
1336
|
+
const base = m.sessionPath.replace(/\\/g, "/").split("/").pop() ?? "session";
|
|
1337
|
+
if (format === "jsonl") {
|
|
1338
|
+
const { readFile } = await import("node:fs/promises");
|
|
1339
|
+
return { filename: base, content: await readFile(m.sessionPath, "utf8"), contentType: "application/jsonl" };
|
|
1340
|
+
}
|
|
1341
|
+
const session = await ensureSession(m);
|
|
1342
|
+
// exportToHtml 返回的是写入磁盘的路径,不是内容;读回后删掉临时副本
|
|
1343
|
+
const outPath = await session.exportToHtml();
|
|
1344
|
+
const { readFile, rm } = await import("node:fs/promises");
|
|
1345
|
+
const content = await readFile(outPath, "utf8");
|
|
1346
|
+
await rm(outPath, { force: true }).catch(() => undefined);
|
|
1347
|
+
return { filename: base.replace(/\.jsonl$/, ".html"), content, contentType: "text/html" };
|
|
1348
|
+
}
|