@huynle/hub 0.6.1 → 0.7.2
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/CHANGELOG.md +870 -0
- package/Dockerfile +33 -0
- package/README.md +1007 -66
- package/package.json +47 -2
- package/packages/claude-code/package.json +12 -0
- package/packages/claude-code/src/bridge.ts +79 -0
- package/packages/claude-code/src/daemon.ts +339 -0
- package/packages/claude-code/src/hook-input.ts +44 -0
- package/packages/claude-code/src/ids.ts +27 -0
- package/packages/claude-code/src/index.ts +18 -0
- package/packages/claude-code/src/launcher.ts +265 -0
- package/packages/claude-code/src/main.ts +127 -0
- package/packages/claude-code/src/mcp.ts +109 -0
- package/packages/claude-code/src/paths.ts +34 -0
- package/packages/claude-code/src/plugin.ts +99 -0
- package/packages/claude-code/src/references.ts +51 -0
- package/packages/claude-code/src/sessions.ts +600 -0
- package/packages/claude-code/src/tools.ts +66 -0
- package/packages/claude-code/src/wire.ts +71 -0
- package/packages/claude-code/src/wrapper.ts +156 -0
- package/packages/cli/dist/hub-cli.js +21647 -0
- package/packages/cli/package.json +11 -0
- package/packages/cli/src/acting.ts +70 -0
- package/packages/cli/src/args.ts +91 -0
- package/packages/cli/src/bootstrap/adapters/archive-stream.ts +276 -0
- package/packages/cli/src/bootstrap/adapters/claude/README.md +63 -0
- package/packages/cli/src/bootstrap/adapters/claude/adapter.ts +250 -0
- package/packages/cli/src/bootstrap/adapters/claude/index.ts +42 -0
- package/packages/cli/src/bootstrap/adapters/claude/node.ts +99 -0
- package/packages/cli/src/bootstrap/adapters/claude/openpgp.ts +221 -0
- package/packages/cli/src/bootstrap/adapters/claude/ports.ts +44 -0
- package/packages/cli/src/bootstrap/adapters/claude/source.ts +160 -0
- package/packages/cli/src/bootstrap/adapters/claude/verify.ts +165 -0
- package/packages/cli/src/bootstrap/adapters/codex/archive.ts +103 -0
- package/packages/cli/src/bootstrap/adapters/codex/index.ts +60 -0
- package/packages/cli/src/bootstrap/adapters/codex/plan.ts +129 -0
- package/packages/cli/src/bootstrap/adapters/codex/sigstore.ts +298 -0
- package/packages/cli/src/bootstrap/adapters/codex/source.ts +102 -0
- package/packages/cli/src/bootstrap/adapters/codex/verify.ts +298 -0
- package/packages/cli/src/bootstrap/adapters/inflate.ts +433 -0
- package/packages/cli/src/bootstrap/adapters/node-services/control.ts +392 -0
- package/packages/cli/src/bootstrap/adapters/node-services/index.ts +23 -0
- package/packages/cli/src/bootstrap/adapters/node-services/launch.ts +223 -0
- package/packages/cli/src/bootstrap/adapters/node-services/plan.ts +222 -0
- package/packages/cli/src/bootstrap/adapters/node-services/render.ts +183 -0
- package/packages/cli/src/bootstrap/adapters/node-services/validate.ts +197 -0
- package/packages/cli/src/bootstrap/adapters/opencode/README.md +204 -0
- package/packages/cli/src/bootstrap/adapters/opencode/apply.ts +205 -0
- package/packages/cli/src/bootstrap/adapters/opencode/archive.ts +91 -0
- package/packages/cli/src/bootstrap/adapters/opencode/config.ts +212 -0
- package/packages/cli/src/bootstrap/adapters/opencode/index.ts +81 -0
- package/packages/cli/src/bootstrap/adapters/opencode/plan.ts +266 -0
- package/packages/cli/src/bootstrap/adapters/opencode/verify.ts +208 -0
- package/packages/cli/src/bootstrap/adapters/readiness.ts +19 -0
- package/packages/cli/src/bootstrap/ansible/index.ts +2 -0
- package/packages/cli/src/bootstrap/ansible/render.ts +279 -0
- package/packages/cli/src/bootstrap/contracts/actions.ts +470 -0
- package/packages/cli/src/bootstrap/contracts/artifact.ts +323 -0
- package/packages/cli/src/bootstrap/contracts/canonical.ts +54 -0
- package/packages/cli/src/bootstrap/contracts/credentials.ts +108 -0
- package/packages/cli/src/bootstrap/contracts/discovery.ts +215 -0
- package/packages/cli/src/bootstrap/contracts/envelope.ts +143 -0
- package/packages/cli/src/bootstrap/contracts/exec.ts +74 -0
- package/packages/cli/src/bootstrap/contracts/harness.ts +202 -0
- package/packages/cli/src/bootstrap/contracts/index.ts +28 -0
- package/packages/cli/src/bootstrap/contracts/journal.ts +198 -0
- package/packages/cli/src/bootstrap/contracts/paths.ts +91 -0
- package/packages/cli/src/bootstrap/contracts/plan.ts +155 -0
- package/packages/cli/src/bootstrap/contracts/platform.ts +62 -0
- package/packages/cli/src/bootstrap/contracts/preview.ts +190 -0
- package/packages/cli/src/bootstrap/contracts/profile.ts +105 -0
- package/packages/cli/src/bootstrap/contracts/secrets.ts +52 -0
- package/packages/cli/src/bootstrap/contracts/validate.ts +212 -0
- package/packages/cli/src/bootstrap/discovery/discover.ts +146 -0
- package/packages/cli/src/bootstrap/discovery/executables.ts +134 -0
- package/packages/cli/src/bootstrap/discovery/host.ts +96 -0
- package/packages/cli/src/bootstrap/discovery/index.ts +6 -0
- package/packages/cli/src/bootstrap/discovery/node-probe.ts +97 -0
- package/packages/cli/src/bootstrap/discovery/probe.ts +41 -0
- package/packages/cli/src/bootstrap/discovery/util.ts +13 -0
- package/packages/cli/src/bootstrap/enrollment/enroll.ts +226 -0
- package/packages/cli/src/bootstrap/enrollment/index.ts +7 -0
- package/packages/cli/src/bootstrap/enrollment/policy.ts +54 -0
- package/packages/cli/src/bootstrap/enrollment/result.ts +63 -0
- package/packages/cli/src/bootstrap/enrollment/status.ts +119 -0
- package/packages/cli/src/bootstrap/enrollment/store.ts +173 -0
- package/packages/cli/src/bootstrap/preview/index.ts +5 -0
- package/packages/cli/src/bootstrap/preview/links.ts +31 -0
- package/packages/cli/src/bootstrap/preview/plan.ts +242 -0
- package/packages/cli/src/bootstrap/preview/preview.ts +104 -0
- package/packages/cli/src/bootstrap/preview/render.ts +30 -0
- package/packages/cli/src/bootstrap/profiles/audit.ts +87 -0
- package/packages/cli/src/bootstrap/profiles/catalog.ts +49 -0
- package/packages/cli/src/bootstrap/profiles/desired.ts +96 -0
- package/packages/cli/src/bootstrap/profiles/drift.ts +286 -0
- package/packages/cli/src/bootstrap/profiles/history.ts +263 -0
- package/packages/cli/src/bootstrap/profiles/index.ts +11 -0
- package/packages/cli/src/bootstrap/profiles/upgrade.ts +276 -0
- package/packages/cli/src/bootstrap/profiles/versions.ts +50 -0
- package/packages/cli/src/bootstrap/release/README.md +115 -0
- package/packages/cli/src/bootstrap/release/assets.ts +39 -0
- package/packages/cli/src/bootstrap/release/cli.ts +332 -0
- package/packages/cli/src/bootstrap/release/harness-pins.json +94 -0
- package/packages/cli/src/bootstrap/release/manifest.ts +118 -0
- package/packages/cli/src/bootstrap/release/pins.ts +192 -0
- package/packages/cli/src/bootstrap/release/text-assets.d.ts +9 -0
- package/packages/cli/src/bootstrap/release/trust-root.json +13 -0
- package/packages/cli/src/bootstrap/release/trust-root.ts +39 -0
- package/packages/cli/src/bootstrap/release/verify.ts +88 -0
- package/packages/cli/src/bootstrap/transaction/builtin.ts +312 -0
- package/packages/cli/src/bootstrap/transaction/engine.ts +994 -0
- package/packages/cli/src/bootstrap/transaction/executor.ts +112 -0
- package/packages/cli/src/bootstrap/transaction/fs.ts +194 -0
- package/packages/cli/src/bootstrap/transaction/index.ts +12 -0
- package/packages/cli/src/bootstrap/transaction/json-merge.ts +403 -0
- package/packages/cli/src/bootstrap/transaction/lock.ts +132 -0
- package/packages/cli/src/bootstrap/transaction/resolve.ts +48 -0
- package/packages/cli/src/bootstrap/transaction/result.ts +60 -0
- package/packages/cli/src/bootstrap/transaction/state.ts +88 -0
- package/packages/cli/src/bootstrap/transaction/store.ts +189 -0
- package/packages/cli/src/bootstrap/transaction/stream.ts +254 -0
- package/packages/cli/src/cli.ts +814 -0
- package/packages/cli/src/credentials.ts +75 -0
- package/packages/cli/src/decisions.ts +106 -0
- package/packages/cli/src/format.ts +185 -0
- package/packages/cli/src/grants.ts +105 -0
- package/packages/cli/src/http.ts +86 -0
- package/packages/cli/src/launches.ts +157 -0
- package/packages/cli/src/login.ts +160 -0
- package/packages/cli/src/main.ts +63 -0
- package/packages/cli/src/mcp.ts +879 -0
- package/packages/cli/src/messaging.ts +108 -0
- package/packages/cli/src/node.ts +279 -0
- package/packages/cli/src/openshell.ts +166 -0
- package/packages/cli/src/setup/commands.ts +809 -0
- package/packages/cli/src/setup/contributors.ts +171 -0
- package/packages/cli/src/setup/dispatch.ts +74 -0
- package/packages/cli/src/setup/launch-driver.ts +109 -0
- package/packages/cli/src/setup/runtime.ts +456 -0
- package/packages/cli/src/setup/spoke-probe.ts +120 -0
- package/packages/cli/src/setup/state.ts +244 -0
- package/packages/cli/src/setup/user-bin.ts +39 -0
- package/packages/cli/src/spoke-services.ts +109 -0
- package/packages/cli/src/watch.ts +28 -0
- package/packages/codex/package.json +12 -0
- package/packages/codex/src/app-server.ts +44 -0
- package/packages/codex/src/ids.ts +28 -0
- package/packages/codex/src/index.ts +11 -0
- package/packages/codex/src/jsonrpc.ts +168 -0
- package/packages/codex/src/main.ts +123 -0
- package/packages/codex/src/paths.ts +65 -0
- package/packages/codex/src/sessions.ts +578 -0
- package/packages/codex/src/spoke.ts +252 -0
- package/packages/codex/src/stdio.ts +101 -0
- package/packages/codex/src/websocket.ts +206 -0
- package/packages/codex/src/wrapper.ts +104 -0
- package/packages/node/package.json +12 -0
- package/packages/node/src/config.ts +184 -0
- package/packages/node/src/connection.ts +283 -0
- package/packages/node/src/daemon.ts +281 -0
- package/packages/node/src/git.ts +107 -0
- package/packages/node/src/harnesses.ts +95 -0
- package/packages/node/src/host.ts +319 -0
- package/packages/node/src/index.ts +17 -0
- package/packages/node/src/main.ts +11 -0
- package/packages/node/src/openshell.ts +73 -0
- package/packages/node/src/run.ts +106 -0
- package/packages/preset/package.json +12 -0
- package/packages/preset/presets/aider.json +8 -0
- package/packages/preset/presets/goose.json +8 -0
- package/packages/preset/src/index.ts +2 -0
- package/packages/preset/src/main.ts +195 -0
- package/packages/preset/src/presets.ts +87 -0
- package/packages/preset/src/spoke.ts +251 -0
- package/packages/preset/src/tmux.ts +160 -0
- package/packages/protocol/package.json +12 -0
- package/packages/protocol/src/harness-events.ts +207 -0
- package/packages/protocol/src/index.ts +1215 -0
- package/packages/protocol/src/launch.ts +172 -0
- package/packages/protocol/src/node.ts +409 -0
- package/packages/protocol/src/preset.ts +144 -0
- package/packages/protocol/src/webhook.ts +62 -0
- package/packages/protocol/src/wire.ts +54 -0
- package/packages/runtime-openshell/image/Dockerfile +36 -0
- package/packages/runtime-openshell/image/README.md +78 -0
- package/packages/runtime-openshell/image/build.ts +203 -0
- package/packages/runtime-openshell/image/entry.sh +163 -0
- package/packages/runtime-openshell/image/image.ts +119 -0
- package/packages/runtime-openshell/image/start.sh +24 -0
- package/packages/runtime-openshell/package.json +12 -0
- package/packages/runtime-openshell/src/cli.ts +584 -0
- package/packages/runtime-openshell/src/index.ts +6 -0
- package/packages/runtime-openshell/src/policy.ts +155 -0
- package/packages/runtime-openshell/src/profiles.ts +181 -0
- package/packages/runtime-openshell/src/runtime.ts +475 -0
- package/packages/runtime-sdk/package.json +12 -0
- package/packages/runtime-sdk/src/index.ts +87 -0
- package/packages/sdk/package.json +12 -0
- package/packages/sdk/src/address.ts +25 -0
- package/packages/sdk/src/client.ts +553 -0
- package/packages/sdk/src/envelope.ts +108 -0
- package/packages/sdk/src/errors.ts +107 -0
- package/packages/sdk/src/index.ts +10 -0
- package/packages/sdk/src/sse.ts +70 -0
- package/packages/sdk/src/types.ts +517 -0
- package/packages/sdk/src/webhook.ts +102 -0
- package/packages/spoke-sdk/package.json +12 -0
- package/packages/spoke-sdk/src/activity.ts +3 -0
- package/packages/spoke-sdk/src/connection.ts +295 -0
- package/packages/spoke-sdk/src/delivery.ts +119 -0
- package/packages/spoke-sdk/src/enroll.ts +101 -0
- package/packages/spoke-sdk/src/harness-hello.ts +79 -0
- package/packages/spoke-sdk/src/harness-spoke.ts +263 -0
- package/packages/spoke-sdk/src/index.ts +12 -0
- package/packages/spoke-sdk/src/prompt-refs.ts +154 -0
- package/packages/spoke-sdk/src/prompt-source.ts +8 -0
- package/packages/spoke-sdk/src/session-ids.ts +49 -0
- package/packages/spoke-sdk/src/session-tokens.ts +78 -0
- package/scripts/hub-claude-code.service +34 -0
- package/scripts/hub-codex-spoke.service +34 -0
- package/scripts/hub-node.service +35 -0
- package/scripts/io.github.huynle.hub-claude-code.plist +53 -0
- package/scripts/io.github.huynle.hub-codex-spoke.plist +53 -0
- package/scripts/io.github.huynle.hub-node.plist +52 -0
- package/src/central/actions.ts +233 -0
- package/src/central/agent/acting-routes.ts +133 -0
- package/src/central/agent/acting-switch.ts +93 -0
- package/src/central/agent/acting.ts +599 -0
- package/src/central/agent/actor.ts +193 -0
- package/src/central/agent/app-tokens.ts +152 -0
- package/src/central/agent/archive-inactive.ts +66 -0
- package/src/central/agent/audit.ts +185 -0
- package/src/central/agent/decisions.ts +530 -0
- package/src/central/agent/delivery.ts +678 -0
- package/src/central/agent/directory-routes.ts +328 -0
- package/src/central/agent/directory.ts +473 -0
- package/src/central/agent/envelope.ts +51 -0
- package/src/central/agent/errors.ts +74 -0
- package/src/central/agent/escalation.ts +219 -0
- package/src/central/agent/event-cursor.ts +49 -0
- package/src/central/agent/event-writers.ts +181 -0
- package/src/central/agent/events-routes.ts +148 -0
- package/src/central/agent/events.ts +277 -0
- package/src/central/agent/grant-requests.ts +221 -0
- package/src/central/agent/grant-routes.ts +70 -0
- package/src/central/agent/grants.ts +235 -0
- package/src/central/agent/launch-engine.ts +1025 -0
- package/src/central/agent/launch-routes.ts +252 -0
- package/src/central/agent/launch-spec.ts +289 -0
- package/src/central/agent/launch-store.ts +264 -0
- package/src/central/agent/launch-tokens.ts +206 -0
- package/src/central/agent/message-limits.ts +123 -0
- package/src/central/agent/message-routes.ts +385 -0
- package/src/central/agent/messages.ts +442 -0
- package/src/central/agent/names.ts +86 -0
- package/src/central/agent/placement.ts +419 -0
- package/src/central/agent/policy.ts +378 -0
- package/src/central/agent/read-routes.ts +385 -0
- package/src/central/agent/routes.ts +281 -0
- package/src/central/agent/session-tokens.ts +227 -0
- package/src/central/agent/versioned.ts +26 -0
- package/src/central/agent/watch.ts +112 -0
- package/src/central/agent/webhook-routes.ts +86 -0
- package/src/central/agent/webhooks.ts +493 -0
- package/src/central/agent-data.ts +177 -0
- package/src/central/auth.ts +350 -121
- package/src/central/backend/file-state.ts +240 -0
- package/src/central/backend/index.ts +116 -0
- package/src/central/backend/memory-bus.ts +93 -0
- package/src/central/backend/memory-catalog.ts +90 -0
- package/src/central/backend/memory-counters.ts +102 -0
- package/src/central/backend/memory-events.ts +45 -0
- package/src/central/backend/memory-kv.ts +38 -0
- package/src/central/backend/memory-state.ts +77 -0
- package/src/central/backend/postgres-migrate.ts +100 -0
- package/src/central/backend/postgres-state.ts +197 -0
- package/src/central/backend/types.ts +246 -0
- package/src/central/backend/valkey-bus.ts +425 -0
- package/src/central/backend/valkey-catalog.ts +190 -0
- package/src/central/backend/valkey-client.ts +36 -0
- package/src/central/backend/valkey-counters.ts +38 -0
- package/src/central/backend/valkey-events.ts +168 -0
- package/src/central/backend/valkey-kv.ts +54 -0
- package/src/central/backend/valkey-locks.ts +92 -0
- package/src/central/cluster/directory.ts +141 -0
- package/src/central/cluster/feed.ts +84 -0
- package/src/central/cluster/leader.ts +232 -0
- package/src/central/cluster/live.ts +151 -0
- package/src/central/cluster/recommend.ts +59 -0
- package/src/central/cluster/rpc.ts +150 -0
- package/src/central/cluster/view.ts +232 -0
- package/src/central/config.ts +197 -3
- package/src/central/done.ts +96 -0
- package/src/central/events-log.ts +100 -0
- package/src/central/http.ts +1149 -170
- package/src/central/import.ts +318 -0
- package/src/central/main.ts +211 -12
- package/src/central/mcp-http.ts +116 -0
- package/src/central/nodes.ts +314 -0
- package/src/central/oidc.ts +34 -14
- package/src/central/push-target.ts +118 -18
- package/src/central/push.ts +256 -82
- package/src/central/registry.ts +902 -92
- package/src/central/say-stream.ts +128 -0
- package/src/central/session-token-push.ts +233 -0
- package/src/central/speech/openai.ts +21 -14
- package/src/central/speech/openrouter.ts +27 -10
- package/src/central/speech/provider.ts +54 -0
- package/src/central/speech/service.ts +22 -7
- package/src/central/spoke-ws.ts +522 -44
- package/src/central/store-shared.ts +518 -0
- package/src/central/store.ts +625 -189
- package/src/central/token-keys.ts +165 -0
- package/src/config.ts +118 -7
- package/src/index.ts +195 -43
- package/src/protocol.ts +3 -650
- package/src/rpc.ts +27 -2
- package/src/spoke/client.ts +3 -272
- package/src/spoke/conversation.ts +16 -12
- package/src/spoke/deliver.ts +75 -0
- package/src/spoke/enroll.ts +2 -79
- package/src/spoke/env-inject.ts +160 -0
- package/src/spoke/hub-rpc.ts +115 -0
- package/src/spoke/hub-tool.ts +409 -0
- package/src/spoke/launch-source.ts +22 -0
- package/src/spoke/launchers.ts +174 -0
- package/src/spoke/locations.ts +3 -2
- package/src/spoke/node-roots.ts +61 -0
- package/src/spoke/prompt-refs.ts +116 -0
- package/src/spoke/session-tokens.ts +2 -0
- package/src/spoke/spoke.ts +465 -28
- package/src/spoke/taskboard.ts +243 -0
- package/src/spoke/timeline.ts +184 -6
- package/src/spoke/tracker.ts +79 -4
- package/src/status.ts +203 -10
- package/src/tui-commands.ts +13 -6
- package/src/tui-messaging.ts +93 -0
- package/src/tui.ts +15 -0
- package/web/dist/apple-touch-icon.png +0 -0
- package/web/dist/assets/highlight-y4rJE6Tt.js +5 -0
- package/web/dist/assets/index-DWnMhXk9.js +20 -0
- package/web/dist/assets/index-DmUXEnw9.css +1 -0
- package/web/dist/assets/ort-wasm-simd-threaded-CxTQ5xH-.wasm +0 -0
- package/web/dist/assets/silero-worker-C8-8vLVS.js +11 -0
- package/web/dist/assets/silero_vad-Dt9-bTDn.onnx +0 -0
- package/web/dist/favicon.ico +0 -0
- package/web/dist/icon-192.png +0 -0
- package/web/dist/icon-512.png +0 -0
- package/web/dist/icon-maskable-512.png +0 -0
- package/web/dist/icon.svg +5 -5
- package/web/dist/index.html +17 -4
- package/web/dist/manifest.webmanifest +2 -2
- package/web/dist/sw.js +79 -15
- package/web/dist/assets/index-DClBvfLL.css +0 -1
- package/web/dist/assets/index-Djo_yF0a.js +0 -12
package/src/status.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// Live status of a voice session and the subagent sessions it spawns, built from OpenCode events.
|
|
2
|
+
import type { JobInfo } from "./protocol"
|
|
2
3
|
|
|
3
4
|
export type SessionState = "idle" | "working" | "blocked" | "error"
|
|
4
5
|
|
|
@@ -6,10 +7,26 @@ export interface Blocker {
|
|
|
6
7
|
readonly kind: "permission" | "form"
|
|
7
8
|
readonly id: string
|
|
8
9
|
readonly summary: string
|
|
10
|
+
/** permission: what OpenCode asks to do ("shell", "edit", "read", an MCP tool), when it said. */
|
|
11
|
+
readonly action?: string
|
|
12
|
+
/** permission: what it applies to (commands, paths), at most BLOCKER_MAX_RESOURCES, each shortened. */
|
|
13
|
+
readonly resources?: readonly string[]
|
|
9
14
|
/** form: asked by the agent's question tool (metadata.kind "question"). */
|
|
10
15
|
readonly question?: true
|
|
11
16
|
/** form: its fields, bounded, so a phone can render and answer it. */
|
|
12
17
|
readonly fields?: readonly FormFieldSpec[]
|
|
18
|
+
/** permission: an agent escalated it (outside its grant) and recommends this (the hub adds it, P3-T8). */
|
|
19
|
+
readonly recommendation?: BlockerRecommendation
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** "@orchestrator recommends Approve: only runs tests in the worktree". */
|
|
23
|
+
export interface BlockerRecommendation {
|
|
24
|
+
/** Who recommends it, as the hub names the agent. */
|
|
25
|
+
readonly by: string
|
|
26
|
+
readonly recommends: "approve"
|
|
27
|
+
readonly why?: string
|
|
28
|
+
/** The Decision the user answers it in. */
|
|
29
|
+
readonly decisionID?: string
|
|
13
30
|
}
|
|
14
31
|
|
|
15
32
|
export interface FormOptionSpec {
|
|
@@ -53,6 +70,75 @@ const FIELD_TYPES = new Set<FormFieldSpec["type"]>(["string", "number", "integer
|
|
|
53
70
|
const text = (v: unknown, max: number) => (typeof v === "string" && v ? v.slice(0, max) : undefined)
|
|
54
71
|
const num = (v: unknown) => (typeof v === "number" && Number.isFinite(v) ? v : undefined)
|
|
55
72
|
|
|
73
|
+
/**
|
|
74
|
+
* What a permission request asks, as OpenCode sent it (permission.asked), kept so a policy can judge it.
|
|
75
|
+
* metadata (diff previews and the like) is dropped. When anything had to be cut or dropped, `truncated` is
|
|
76
|
+
* set: a judgement on what is left does not cover the whole request.
|
|
77
|
+
*/
|
|
78
|
+
export interface PermissionRequest {
|
|
79
|
+
/** "shell", "read", "edit", "external_directory", an MCP tool ("<server>_<tool>"), … */
|
|
80
|
+
readonly action: string
|
|
81
|
+
/** What the action applies to (one per parsed shell command, paths, "*"). */
|
|
82
|
+
readonly resources: readonly string[]
|
|
83
|
+
/** The patterns an "always" reply would save as rules. */
|
|
84
|
+
readonly save?: readonly string[]
|
|
85
|
+
/** The tool call that asked. */
|
|
86
|
+
readonly source?: { readonly type: "tool"; readonly messageID: string; readonly id: string }
|
|
87
|
+
readonly truncated?: true
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Resources (and save patterns) kept per request, and the length kept of each. */
|
|
91
|
+
export const PERMISSION_MAX_RESOURCES = 100
|
|
92
|
+
export const PERMISSION_TEXT_MAX = 4000
|
|
93
|
+
/** Longest action kept; a longer one makes the request unusable (undefined). */
|
|
94
|
+
const PERMISSION_ACTION_MAX = 200
|
|
95
|
+
const SOURCE_ID_MAX = 200
|
|
96
|
+
/** Resources shown on a blocker (catalog and status go to every console), and the length of each. */
|
|
97
|
+
export const BLOCKER_MAX_RESOURCES = 10
|
|
98
|
+
const BLOCKER_RESOURCE_MAX = 500
|
|
99
|
+
|
|
100
|
+
/** Strings of `list`, bounded; `cut` is set when anything was dropped or shortened. */
|
|
101
|
+
function boundedStrings(list: unknown, maxItems: number, maxLength: number): { items: string[]; cut: boolean } {
|
|
102
|
+
if (!Array.isArray(list)) return { items: [], cut: list !== undefined }
|
|
103
|
+
const strings = list.filter((v): v is string => typeof v === "string")
|
|
104
|
+
const items = strings.slice(0, maxItems).map((v) => v.slice(0, maxLength))
|
|
105
|
+
const cut = strings.length !== list.length || strings.length > maxItems || strings.some((v) => v.length > maxLength)
|
|
106
|
+
return { items, cut }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** A permission.asked event's request, bounded; undefined without a usable action. */
|
|
110
|
+
export function permissionRequest(raw: any): PermissionRequest | undefined {
|
|
111
|
+
const action = raw?.action
|
|
112
|
+
if (typeof action !== "string" || !action || action.length > PERMISSION_ACTION_MAX) return undefined
|
|
113
|
+
const resources = boundedStrings(raw.resources, PERMISSION_MAX_RESOURCES, PERMISSION_TEXT_MAX)
|
|
114
|
+
const save = raw.save === undefined ? undefined : boundedStrings(raw.save, PERMISSION_MAX_RESOURCES, PERMISSION_TEXT_MAX)
|
|
115
|
+
const src = raw.source
|
|
116
|
+
const source =
|
|
117
|
+
src?.type === "tool" && typeof src.messageID === "string" && typeof src.id === "string"
|
|
118
|
+
? { type: "tool" as const, messageID: src.messageID.slice(0, SOURCE_ID_MAX), id: src.id.slice(0, SOURCE_ID_MAX) }
|
|
119
|
+
: undefined
|
|
120
|
+
return {
|
|
121
|
+
action,
|
|
122
|
+
resources: resources.items,
|
|
123
|
+
...(save ? { save: save.items } : {}),
|
|
124
|
+
...(source ? { source } : {}),
|
|
125
|
+
...(resources.cut || save?.cut ? { truncated: true as const } : {}),
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** A permission.asked event as a blocker: its summary, and what it asks (bounded for display). */
|
|
130
|
+
function permissionBlocker(data: Record<string, any>): Blocker {
|
|
131
|
+
const action = typeof data.action === "string" && data.action ? data.action.slice(0, PERMISSION_ACTION_MAX) : undefined
|
|
132
|
+
const resources = boundedStrings(data.resources, BLOCKER_MAX_RESOURCES, BLOCKER_RESOURCE_MAX).items
|
|
133
|
+
return {
|
|
134
|
+
kind: "permission",
|
|
135
|
+
id: data.id,
|
|
136
|
+
summary: [data.action, (data.resources ?? []).join(", ")].filter(Boolean).join(": "),
|
|
137
|
+
...(action ? { action } : {}),
|
|
138
|
+
...(resources.length ? { resources } : {}),
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
56
142
|
function optionSpec(raw: any): FormOptionSpec | undefined {
|
|
57
143
|
const value = text(raw?.value, FORM_LABEL_MAX)
|
|
58
144
|
if (value === undefined) return undefined
|
|
@@ -112,15 +198,28 @@ export interface SessionStatus {
|
|
|
112
198
|
readonly state: SessionState
|
|
113
199
|
readonly detail?: string
|
|
114
200
|
readonly blockers: ReadonlyArray<Blocker>
|
|
201
|
+
/** OpenCode is retrying the model request (state stays "working"). */
|
|
202
|
+
readonly retry?: { readonly attempt?: number; readonly message?: string }
|
|
203
|
+
/** Shells and terminals of the session: running ones, then the last few that ended (oldest first). */
|
|
204
|
+
readonly jobs?: readonly JobInfo[]
|
|
115
205
|
}
|
|
116
206
|
|
|
207
|
+
/** Ended jobs kept per session (shown briefly with their exit code). */
|
|
208
|
+
export const ENDED_JOBS_KEPT = 5
|
|
209
|
+
/** Jobs kept per session at most (running ones beyond it are dropped oldest first). */
|
|
210
|
+
const JOBS_MAX = 50
|
|
211
|
+
|
|
117
212
|
interface Entry {
|
|
118
213
|
id: string
|
|
119
214
|
parentID?: string
|
|
120
215
|
title: string
|
|
121
216
|
running: boolean
|
|
217
|
+
/** session.status: busy or retry (OpenCode's own view of the session). */
|
|
218
|
+
busy: boolean
|
|
219
|
+
retry?: { attempt?: number; message?: string }
|
|
122
220
|
error?: string
|
|
123
221
|
blockers: Blocker[]
|
|
222
|
+
jobs: JobInfo[]
|
|
124
223
|
}
|
|
125
224
|
|
|
126
225
|
interface EventLike {
|
|
@@ -132,7 +231,7 @@ export class StatusBoard {
|
|
|
132
231
|
private readonly sessions = new Map<string, Entry>()
|
|
133
232
|
|
|
134
233
|
constructor(rootID: string, title: string) {
|
|
135
|
-
this.sessions.set(rootID, { id: rootID, title, running: false, blockers: [] })
|
|
234
|
+
this.sessions.set(rootID, { id: rootID, title, running: false, busy: false, blockers: [], jobs: [] })
|
|
136
235
|
}
|
|
137
236
|
|
|
138
237
|
/** Applies an event. Returns true when the visible status changed. */
|
|
@@ -146,7 +245,9 @@ export class StatusBoard {
|
|
|
146
245
|
parentID: data.parentID,
|
|
147
246
|
title: data.title ?? data.agent ?? data.slug ?? "subagent",
|
|
148
247
|
running: false,
|
|
248
|
+
busy: false,
|
|
149
249
|
blockers: [],
|
|
250
|
+
jobs: [],
|
|
150
251
|
})
|
|
151
252
|
return true
|
|
152
253
|
}
|
|
@@ -161,20 +262,16 @@ export class StatusBoard {
|
|
|
161
262
|
})
|
|
162
263
|
case "session.execution.succeeded":
|
|
163
264
|
case "session.execution.interrupted":
|
|
164
|
-
return this.update(data.sessionID, (entry) => (entry
|
|
265
|
+
return this.update(data.sessionID, (entry) => endRun(entry))
|
|
165
266
|
case "session.execution.failed":
|
|
166
267
|
return this.update(data.sessionID, (entry) => {
|
|
167
|
-
entry
|
|
268
|
+
endRun(entry)
|
|
168
269
|
entry.error = String(data.error?.message ?? "failed")
|
|
169
270
|
})
|
|
170
271
|
case "permission.asked":
|
|
171
272
|
return this.update(data.sessionID, (entry) => {
|
|
172
273
|
if (entry.blockers.some((b) => b.id === data.id)) return
|
|
173
|
-
entry.blockers.push(
|
|
174
|
-
kind: "permission",
|
|
175
|
-
id: data.id,
|
|
176
|
-
summary: [data.action, (data.resources ?? []).join(", ")].filter(Boolean).join(": "),
|
|
177
|
-
})
|
|
274
|
+
entry.blockers.push(permissionBlocker(data))
|
|
178
275
|
})
|
|
179
276
|
case "permission.replied":
|
|
180
277
|
return this.unblock(data.sessionID, data.requestID)
|
|
@@ -186,6 +283,30 @@ export class StatusBoard {
|
|
|
186
283
|
case "form.replied":
|
|
187
284
|
case "form.cancelled":
|
|
188
285
|
return this.unblock(data.sessionID, data.id)
|
|
286
|
+
case "session.status":
|
|
287
|
+
return this.update(data.sessionID, (entry) => setStatus(entry, data.status))
|
|
288
|
+
case "session.seed":
|
|
289
|
+
return this.update(data.sessionID, (entry) => {
|
|
290
|
+
setStatus(entry, data.status)
|
|
291
|
+
entry.jobs = entry.jobs.filter((job) => job.status !== "running")
|
|
292
|
+
for (const job of Array.isArray(data.jobs) ? data.jobs : []) if (isJob(job)) upsertJob(entry, job)
|
|
293
|
+
})
|
|
294
|
+
case "shell.created":
|
|
295
|
+
case "session.shell.started":
|
|
296
|
+
case "pty.created":
|
|
297
|
+
case "pty.updated":
|
|
298
|
+
case "shell.exited":
|
|
299
|
+
case "session.shell.ended":
|
|
300
|
+
case "pty.exited":
|
|
301
|
+
return this.update(data.sessionID, (entry) => {
|
|
302
|
+
const job = jobOf(data, entry.jobs.find((j) => j.id === data.id))
|
|
303
|
+
if (job) upsertJob(entry, job)
|
|
304
|
+
})
|
|
305
|
+
case "shell.deleted":
|
|
306
|
+
case "pty.deleted":
|
|
307
|
+
return this.update(data.sessionID, (entry) => {
|
|
308
|
+
entry.jobs = entry.jobs.filter((job) => job.id !== data.id)
|
|
309
|
+
})
|
|
189
310
|
default:
|
|
190
311
|
return false
|
|
191
312
|
}
|
|
@@ -200,6 +321,18 @@ export class StatusBoard {
|
|
|
200
321
|
return [...this.sessions.keys()]
|
|
201
322
|
}
|
|
202
323
|
|
|
324
|
+
/**
|
|
325
|
+
* True when nothing in the tree runs (no run, busy or retrying status), waits for an answer, or has a running
|
|
326
|
+
* shell job. Terminals (pty) do not count: an open terminal is not work in progress.
|
|
327
|
+
*/
|
|
328
|
+
settled(): boolean {
|
|
329
|
+
for (const entry of this.sessions.values()) {
|
|
330
|
+
if (entry.running || entry.busy || entry.blockers.length) return false
|
|
331
|
+
if (entry.jobs.some((job) => job.status === "running" && job.kind === "shell")) return false
|
|
332
|
+
}
|
|
333
|
+
return true
|
|
334
|
+
}
|
|
335
|
+
|
|
203
336
|
snapshot(): { sessions: SessionStatus[] } {
|
|
204
337
|
return { sessions: [...this.sessions.values()].map(view) }
|
|
205
338
|
}
|
|
@@ -218,15 +351,75 @@ export class StatusBoard {
|
|
|
218
351
|
}
|
|
219
352
|
}
|
|
220
353
|
|
|
354
|
+
const JOB_STATUSES = new Set(["running", "exited", "timeout", "killed"])
|
|
355
|
+
|
|
356
|
+
function isJob(v: any): v is JobInfo {
|
|
357
|
+
return typeof v?.id === "string" && v.id && (v.kind === "shell" || v.kind === "pty") && JOB_STATUSES.has(v.status)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** A job event's data (compact, see compactActivity) merged over what is known of the job. */
|
|
361
|
+
function jobOf(data: Record<string, any>, known: JobInfo | undefined): JobInfo | undefined {
|
|
362
|
+
if (typeof data.id !== "string" || !JOB_STATUSES.has(data.status)) return undefined
|
|
363
|
+
const kind = data.kind === "pty" ? "pty" : "shell"
|
|
364
|
+
const ended = data.status !== "running"
|
|
365
|
+
const at = num(data.at)
|
|
366
|
+
const command = text(data.command, 200) ?? known?.command
|
|
367
|
+
const exit = num(data.exit) ?? (ended ? known?.exit : undefined)
|
|
368
|
+
const started = known?.started ?? (!ended ? at : undefined)
|
|
369
|
+
return {
|
|
370
|
+
id: data.id,
|
|
371
|
+
kind,
|
|
372
|
+
...(command ? { command } : {}),
|
|
373
|
+
status: data.status,
|
|
374
|
+
...(exit !== undefined ? { exit } : {}),
|
|
375
|
+
...(data.background === true || known?.background ? { background: true as const } : {}),
|
|
376
|
+
...(started !== undefined ? { started } : {}),
|
|
377
|
+
...(ended && at !== undefined ? { ended: at } : {}),
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function upsertJob(entry: Entry, job: JobInfo): void {
|
|
382
|
+
const rest = entry.jobs.filter((j) => j.id !== job.id)
|
|
383
|
+
rest.push({ ...job })
|
|
384
|
+
const running = rest.filter((j) => j.status === "running")
|
|
385
|
+
const endedJobs = rest.filter((j) => j.status !== "running").slice(-ENDED_JOBS_KEPT)
|
|
386
|
+
const keep = new Set([...running.slice(-JOBS_MAX), ...endedJobs])
|
|
387
|
+
entry.jobs = rest.filter((j) => keep.has(j))
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** A run ended: OpenCode v2 sends no idle status, so a busy or retrying status (e.g. seeded) ends with it. */
|
|
391
|
+
function endRun(entry: Entry): void {
|
|
392
|
+
entry.running = false
|
|
393
|
+
entry.busy = false
|
|
394
|
+
entry.retry = undefined
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function setStatus(entry: Entry, status: any): void {
|
|
398
|
+
const type = status?.type
|
|
399
|
+
if (type !== "idle" && type !== "busy" && type !== "retry") return
|
|
400
|
+
entry.busy = type !== "idle"
|
|
401
|
+
if (type === "retry") {
|
|
402
|
+
const attempt = num(status.attempt)
|
|
403
|
+
const message = text(status.message, 200)
|
|
404
|
+
entry.retry = { ...(attempt !== undefined ? { attempt } : {}), ...(message ? { message } : {}) }
|
|
405
|
+
} else entry.retry = undefined
|
|
406
|
+
if (type === "busy") entry.error = undefined
|
|
407
|
+
}
|
|
408
|
+
|
|
221
409
|
function view(entry: Entry): SessionStatus {
|
|
410
|
+
const working = entry.running || entry.busy || entry.jobs.some((job) => job.status === "running")
|
|
222
411
|
const state: SessionState =
|
|
223
|
-
entry.blockers.length > 0 ? "blocked" : entry.error ? "error" :
|
|
412
|
+
entry.blockers.length > 0 ? "blocked" : entry.error && !working ? "error" : working ? "working" : "idle"
|
|
413
|
+
const retry = entry.retry && state === "working" ? entry.retry : undefined
|
|
414
|
+
const detail = state === "error" ? entry.error : retry ? (retry.message ? `Retrying: ${retry.message}` : "Retrying") : undefined
|
|
224
415
|
return {
|
|
225
416
|
id: entry.id,
|
|
226
417
|
...(entry.parentID ? { parentID: entry.parentID } : {}),
|
|
227
418
|
title: entry.title,
|
|
228
419
|
state,
|
|
229
|
-
...(
|
|
420
|
+
...(detail ? { detail } : {}),
|
|
230
421
|
blockers: entry.blockers.map((blocker) => structuredClone(blocker)),
|
|
422
|
+
...(retry ? { retry: { ...retry } } : {}),
|
|
423
|
+
...(entry.jobs.length ? { jobs: entry.jobs.map((job) => ({ ...job })) } : {}),
|
|
231
424
|
}
|
|
232
425
|
}
|
package/src/tui-commands.ts
CHANGED
|
@@ -14,7 +14,13 @@ export interface HubCommand {
|
|
|
14
14
|
readonly slash: string
|
|
15
15
|
/** Shown in the command palette after "Hub: ". */
|
|
16
16
|
readonly title: string
|
|
17
|
-
|
|
17
|
+
/**
|
|
18
|
+
* The command takes arguments: `/<slash> text` runs it with the raw text after the name (quotes and inner
|
|
19
|
+
* spacing kept). Without it, any text after the name sends the whole line to the model instead.
|
|
20
|
+
*/
|
|
21
|
+
readonly arguments?: boolean
|
|
22
|
+
/** `input`: the raw arguments; undefined (palette, key binding) and "" (no text) both mean none. */
|
|
23
|
+
readonly run: (input?: string) => Promise<unknown>
|
|
18
24
|
}
|
|
19
25
|
|
|
20
26
|
/** The shape OpenCode's keymap layer takes for one command. */
|
|
@@ -23,8 +29,8 @@ export interface KeymapCommand {
|
|
|
23
29
|
readonly title: string
|
|
24
30
|
readonly group: string
|
|
25
31
|
readonly palette: true
|
|
26
|
-
readonly slash: { readonly name: string }
|
|
27
|
-
readonly run: () => void
|
|
32
|
+
readonly slash: { readonly name: string; readonly arguments?: true }
|
|
33
|
+
readonly run: (input?: string) => void
|
|
28
34
|
}
|
|
29
35
|
|
|
30
36
|
/**
|
|
@@ -44,9 +50,10 @@ export function hubCommands(features: readonly HubCommand[], onError: (cause: un
|
|
|
44
50
|
title: `${COMMAND_GROUP}: ${feature.title}`,
|
|
45
51
|
group: COMMAND_GROUP,
|
|
46
52
|
palette: true,
|
|
47
|
-
slash: { name: feature.slash },
|
|
48
|
-
|
|
49
|
-
|
|
53
|
+
slash: feature.arguments ? { name: feature.slash, arguments: true } : { name: feature.slash },
|
|
54
|
+
// Keyboard dispatch may pass something other than text; only a string is the command's input.
|
|
55
|
+
run: (input?: unknown) => {
|
|
56
|
+
void feature.run(typeof input === "string" ? input : undefined).catch(onError)
|
|
50
57
|
},
|
|
51
58
|
}
|
|
52
59
|
})
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// /who and /msg in the TUI: what the user typed after the slash name, and what the TUI shows back. The server side
|
|
2
|
+
// (src/spoke/hub-rpc.ts) talks to the hub as the TUI's current session; labels arrive sanitised from there.
|
|
3
|
+
import type { WhoRow } from "./spoke/hub-rpc"
|
|
4
|
+
|
|
5
|
+
/** One session in /who, as the server's hubWho RPC returns it (type only: the TUI never loads the SDK). */
|
|
6
|
+
export type { WhoRow }
|
|
7
|
+
|
|
8
|
+
export const MSG_USAGE = "Usage: /msg @name text (or @@role, @name.project, ses_…, @me)"
|
|
9
|
+
|
|
10
|
+
/** @name, @@role, @name.project, @me, or a session ID. */
|
|
11
|
+
const ADDRESS = /^(@@?[^\s@]\S*|ses_\S+)$/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* `/msg` arguments: the first word is the address, everything after the whitespace that follows it is the text,
|
|
15
|
+
* word for word (spacing, quotes and new lines kept). Undefined when there are no arguments at all.
|
|
16
|
+
*/
|
|
17
|
+
export function parseMsgInput(input?: string): { to: string; text: string } | { error: string } | undefined {
|
|
18
|
+
if (input === undefined || !input.trim()) return undefined
|
|
19
|
+
const match = /^\s*(\S+)(?:\s+([\s\S]*))?$/.exec(input)
|
|
20
|
+
const to = match?.[1] ?? ""
|
|
21
|
+
const text = match?.[2] ?? ""
|
|
22
|
+
if (!ADDRESS.test(to)) return { error: `Start with the address. ${MSG_USAGE}` }
|
|
23
|
+
if (!text.trim()) return { error: `What should the message say? ${MSG_USAGE}` }
|
|
24
|
+
return { to, text }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** `/who` arguments: nothing (everyone), a project, or a @@role. */
|
|
28
|
+
export function parseWhoInput(input?: string): { project?: string; role?: string } {
|
|
29
|
+
const value = input?.trim() ?? ""
|
|
30
|
+
if (!value) return {}
|
|
31
|
+
if (value.startsWith("@@") && value.length > 2) return { role: value.slice(2) }
|
|
32
|
+
return { project: value }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** One line per session: name (else ID) · project · state · online/offline · roles. */
|
|
36
|
+
export function formatWho(rows: readonly WhoRow[]): string {
|
|
37
|
+
if (!rows.length) return "No named or role-holding sessions."
|
|
38
|
+
return rows
|
|
39
|
+
.map((s) =>
|
|
40
|
+
[s.name ? `@${s.name}` : s.sessionID, s.project ?? "", s.state ?? "", s.online ? "online" : "offline", s.roles.map((r) => `@@${r}`).join(" ")]
|
|
41
|
+
.filter(Boolean)
|
|
42
|
+
.join(" · "),
|
|
43
|
+
)
|
|
44
|
+
.join("\n")
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The toast after /msg: who it reached and how ("stored" when the hub only stored it). */
|
|
48
|
+
export function formatSent(sent: { id: string; to: string; delivery?: string }): string {
|
|
49
|
+
return `Sent to ${sent.to} (${sent.delivery ?? "stored"})`
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** What /who and /msg need from the TUI (src/tui.ts): the current session, the RPC calls and the UI. */
|
|
53
|
+
export interface MessagingDeps {
|
|
54
|
+
/** The session the TUI shows; undefined on the home screen. */
|
|
55
|
+
sessionID(): string | undefined
|
|
56
|
+
who(input: { sessionID: string; project?: string; role?: string }): Promise<{ sessions: WhoRow[] }>
|
|
57
|
+
msg(input: { sessionID: string; to: string; text: string }): Promise<{ id: string; to: string; delivery?: string }>
|
|
58
|
+
prompt(options: { title: string; description?: string; placeholder?: string; value?: string }): Promise<string | null | undefined>
|
|
59
|
+
alert(options: { title: string; message: string }): Promise<unknown>
|
|
60
|
+
toast(message: string, variant?: "info" | "success" | "warning" | "error"): unknown
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const NO_SESSION = (what: string) => `Open a session first: ${what} acts as the current session.`
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* /msg: sends as the current session. Without arguments (palette, bare /msg) it asks for "@name text"; RPC failures
|
|
67
|
+
* are thrown (the command registry shows them as a toast).
|
|
68
|
+
*/
|
|
69
|
+
export async function runMsg(deps: MessagingDeps, input?: string): Promise<void> {
|
|
70
|
+
const sessionID = deps.sessionID()
|
|
71
|
+
if (!sessionID) return void deps.toast(NO_SESSION("/msg"), "warning")
|
|
72
|
+
let parsed = parseMsgInput(input)
|
|
73
|
+
if (!parsed) {
|
|
74
|
+
const typed = await deps.prompt({ title: "Message a session", description: `Sent as this session. ${MSG_USAGE}`, placeholder: "@name text" })
|
|
75
|
+
if (typeof typed !== "string") return
|
|
76
|
+
parsed = parseMsgInput(typed)
|
|
77
|
+
if (!parsed) return
|
|
78
|
+
}
|
|
79
|
+
if ("error" in parsed) return void deps.toast(parsed.error, "error")
|
|
80
|
+
const sent = await deps.msg({ sessionID, to: parsed.to, text: parsed.text })
|
|
81
|
+
deps.toast(formatSent(sent), "success")
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** /who [project | @@role]: the hub's named and role-holding sessions in a dialog, asked as the current session. */
|
|
85
|
+
export async function runWho(deps: MessagingDeps, input?: string): Promise<void> {
|
|
86
|
+
const sessionID = deps.sessionID()
|
|
87
|
+
if (!sessionID) return void deps.toast(NO_SESSION("/who"), "warning")
|
|
88
|
+
const filter = parseWhoInput(input)
|
|
89
|
+
const { sessions } = await deps.who({ sessionID, ...filter })
|
|
90
|
+
if (!sessions.length) return void deps.toast(formatWho(sessions), "info")
|
|
91
|
+
const scope = filter.role ? ` · @@${filter.role}` : filter.project ? ` · ${filter.project}` : ""
|
|
92
|
+
await deps.alert({ title: `Hub · who${scope}`, message: formatWho(sessions) })
|
|
93
|
+
}
|
package/src/tui.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { HubRpc } from "./rpc"
|
|
|
5
5
|
import { localServerFor, modeFor, needsSetup, parseServerInput, setupOptions, type SetupChoice } from "./setup"
|
|
6
6
|
import { hubCommands } from "./tui-commands"
|
|
7
7
|
import { FormRelay } from "./tui-forms"
|
|
8
|
+
import { runMsg, runWho, type MessagingDeps } from "./tui-messaging"
|
|
8
9
|
import { PresenceReporter, shownSessions } from "./tui-presence"
|
|
9
10
|
import { startVoice } from "./voice-command"
|
|
10
11
|
|
|
@@ -15,6 +16,8 @@ import { startVoice } from "./voice-command"
|
|
|
15
16
|
// /voice: no menu — connect (setting up first when needed), share this session, and open it in the browser
|
|
16
17
|
// with Voice mode starting (src/voice-command.ts). Links are shown here, never posted into the session, so
|
|
17
18
|
// they do not reach the model. Decisions live in src/setup.ts and src/voice-command.ts (tested).
|
|
19
|
+
// /who [project | @@role] and /msg @name text: other sessions on the hub, as the current session
|
|
20
|
+
// (src/tui-messaging.ts; server side src/spoke/hub-rpc.ts).
|
|
18
21
|
|
|
19
22
|
type Mode = "off" | "always" | "session"
|
|
20
23
|
interface Status {
|
|
@@ -408,6 +411,16 @@ export default Plugin.define({
|
|
|
408
411
|
toast,
|
|
409
412
|
})
|
|
410
413
|
|
|
414
|
+
// /who and /msg: the server sends as this TUI's current session, with that session's own hub token.
|
|
415
|
+
const messaging: MessagingDeps = {
|
|
416
|
+
sessionID: currentSession,
|
|
417
|
+
who: (input) => call("hubWho", input),
|
|
418
|
+
msg: (input) => call("hubMsg", input),
|
|
419
|
+
prompt: (options) => context.ui.dialog.prompt(options),
|
|
420
|
+
alert: (options) => context.ui.dialog.alert(options),
|
|
421
|
+
toast: (message, variant) => toast(message, variant),
|
|
422
|
+
}
|
|
423
|
+
|
|
411
424
|
// Presence: tell the server-side spoke which sessions this TUI shows, so the phone lists them as open.
|
|
412
425
|
const reporter = `tui_${Math.random().toString(36).slice(2, 12)}`
|
|
413
426
|
const presence = new PresenceReporter({
|
|
@@ -436,6 +449,8 @@ export default Plugin.define({
|
|
|
436
449
|
[
|
|
437
450
|
{ id: "open", slash: "hub", title: "server, connection, sharing and phones", run: open },
|
|
438
451
|
{ id: "voice", slash: "voice", title: "talk to this session (Voice mode in the browser)", run: voice },
|
|
452
|
+
{ id: "who", slash: "who", title: "who is on the hub (names and roles)", arguments: true, run: (input) => runWho(messaging, input) },
|
|
453
|
+
{ id: "msg", slash: "msg", title: "message a session as this session (/msg @name text)", arguments: true, run: (input) => runMsg(messaging, input) },
|
|
439
454
|
],
|
|
440
455
|
(cause) => toast(errorMessage(cause), "error"),
|
|
441
456
|
)
|
|
Binary file
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
var e={ts:`ts`,typescript:`ts`,tsx:`ts`,mts:`ts`,cts:`ts`,js:`ts`,javascript:`ts`,jsx:`ts`,mjs:`ts`,cjs:`ts`,node:`ts`,json:`json`,jsonc:`json`,json5:`json`,bash:`bash`,sh:`bash`,shell:`bash`,zsh:`bash`,console:`bash`,shellsession:`bash`,fish:`bash`,python:`python`,py:`python`,python3:`python`,go:`go`,golang:`go`,rust:`rust`,rs:`rust`,yaml:`yaml`,yml:`yaml`,diff:`diff`,patch:`diff`,sql:`sql`,postgresql:`sql`,postgres:`sql`,mysql:`sql`,sqlite:`sql`,css:`css`,scss:`css`,less:`css`,html:`html`,htm:`html`,xml:`html`,svg:`html`,vue:`html`,md:`md`,markdown:`md`};function t(t){return e[String(t??``).trim().toLowerCase()]}var n=e=>new Set(e.split(` `)),r=e=>e>=`a`&&e<=`z`||e>=`A`&&e<=`Z`||e===`_`||e===`$`,i=e=>e!==void 0&&(r(e)||e>=`0`&&e<=`9`);function a(e,t){let n=t+1;for(;i(e[n]);)n++;return e.slice(t,n)}var o=/(?:0[xX][\da-fA-F_]+|0[bB][01_]+|0[oO][0-7_]+|\d[\d_]*(?:\.\d[\d_]*)?(?:[eE][+-]?\d+)?)[a-zA-Z]{0,3}\b/y,s=/"(?:[^"\\\n]|\\.)*"?/y,c=/'(?:[^'\\\n]|\\.)*'?/y,l=/\/\*[\s\S]*?(?:\*\/|$)/y,u=/\/\/[^\n]*/y,d=/#[^\n]*/y,f={ts:{rules:[{kind:`comment`,re:u,first:`/`},{kind:`comment`,re:l,first:`/`},{kind:`string`,re:/`(?:[^`\\]|\\[\s\S])*`?/y,first:"`"},{kind:`string`,re:s,first:`"`},{kind:`string`,re:c,first:`'`},{kind:`number`,re:o,first:`0123456789`}],keywords:n(`abstract as async await break case catch class const continue debugger declare default delete do else enum export extends false finally for from function get if implements import in instanceof interface keyof let new null of private protected public readonly return satisfies set static super switch this throw true try type typeof undefined var void while with yield`),types:n(`string number boolean bigint symbol object unknown never any void Promise Array Record Map Set Partial Readonly`)},json:{rules:[{kind:`comment`,re:u,first:`/`},{kind:`comment`,re:l,first:`/`},{kind:`prop`,re:/"(?:[^"\\\n]|\\.)*"(?=\s*:)/y,first:`"`},{kind:`string`,re:s,first:`"`},{kind:`number`,re:/-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/y,first:`-0123456789`}],keywords:n(`true false null`)},bash:{rules:[{kind:`comment`,re:d,at:`word`,first:`#`},{kind:`string`,re:/"(?:[^"\\]|\\[\s\S])*"?/y,first:`"`},{kind:`string`,re:/'[^']*'?/y,first:`'`},{kind:`prop`,re:/\$(?:\{[^}\n]*\}?|[\w@#?$!*-]+)/y,first:`$`},{kind:`fn`,re:/[\w./-]+/y,at:`command`},{kind:`attr`,re:/--?[\w-]+/y,at:`word`,first:`-`}],keywords:n(`if then else elif fi for do done case esac while until function in export local return select time sudo`)},python:{rules:[{kind:`comment`,re:d,first:`#`},{kind:`string`,re:/[rRbBfFuU]{0,2}(?:"""[\s\S]*?(?:"""|$)|'''[\s\S]*?(?:'''|$))/y,first:`rRbBfFuU"'`},{kind:`string`,re:/[rRbBfFuU]{0,2}"(?:[^"\\\n]|\\.)*"?/y,first:`rRbBfFuU"'`},{kind:`string`,re:/[rRbBfFuU]{0,2}'(?:[^'\\\n]|\\.)*'?/y,first:`rRbBfFuU"'`},{kind:`meta`,re:/@[\w.]+/y,first:`@`},{kind:`number`,re:o,first:`0123456789`}],keywords:n(`and as assert async await break class continue def del elif else except False finally for from global if import in is lambda None nonlocal not or pass raise return self True try while with yield match case`),types:n(`int str float bool list dict set tuple bytes object type`)},go:{rules:[{kind:`comment`,re:u,first:`/`},{kind:`comment`,re:l,first:`/`},{kind:`string`,re:/`[^`]*`?/y,first:"`"},{kind:`string`,re:s,first:`"`},{kind:`string`,re:c,first:`'`},{kind:`number`,re:o,first:`0123456789`}],keywords:n(`break case chan const continue default defer else fallthrough for func go goto if import interface map package range return select struct switch type var nil true false iota`),types:n(`string int int8 int16 int32 int64 uint uint8 uint16 uint32 uint64 uintptr byte rune float32 float64 bool error any`)},rust:{rules:[{kind:`comment`,re:u,first:`/`},{kind:`comment`,re:l,first:`/`},{kind:`string`,re:/b?r#*"[\s\S]*?(?:"#*|$)/y,first:`br`},{kind:`string`,re:/b?"(?:[^"\\]|\\[\s\S])*"?/y,first:`b"`},{kind:`string`,re:/b?'(?:[^'\\\n]|\\.){1,8}'/y,first:`b'`},{kind:`meta`,re:/#!?\[[^\]\n]*\]?/y,first:`#`},{kind:`number`,re:o,first:`0123456789`}],keywords:n(`as async await break const continue crate dyn else enum extern false fn for if impl in let loop match mod move mut pub ref return self Self static struct super trait true type unsafe use where while`),types:n(`i8 i16 i32 i64 i128 isize u8 u16 u32 u64 u128 usize f32 f64 bool char str String Vec Option Result Box Some None Ok Err`),macros:!0},yaml:{rules:[{kind:`comment`,re:d,at:`word`,first:`#`},{kind:`prop`,re:/[^\s#:'"][^\n:#]*?(?=\s*:(?:\s|$))/y,at:`line`},{kind:`string`,re:s,first:`"`},{kind:`string`,re:/'(?:[^'\n]|'')*'?/y,first:`'`},{kind:`meta`,re:/---|\.\.\./y,at:`line`,first:`-.`},{kind:`number`,re:/-?\d+(?:\.\d+)?\b/y,first:`-0123456789`}],keywords:n(`true false null yes no on off`)},sql:{rules:[{kind:`comment`,re:/--[^\n]*/y,first:`-`},{kind:`comment`,re:l,first:`/`},{kind:`string`,re:/'(?:[^']|'')*'?/y,first:`'`},{kind:`prop`,re:/"[^"\n]*"?/y,first:`"`},{kind:`number`,re:/\d+(?:\.\d+)?\b/y,first:`0123456789`}],keywords:n(`select from where and or not insert into values update set delete create table drop alter add column index primary key foreign references join left right inner outer full on as group by order having limit offset distinct union all null is in like between case when then else end exists returning with default unique check if begin commit rollback asc desc cascade view trigger`),types:n(`int integer bigint smallint serial text varchar char boolean bool date timestamp timestamptz json jsonb uuid real numeric float double decimal`),ci:!0},css:{rules:[{kind:`comment`,re:l,first:`/`},{kind:`string`,re:s,first:`"`},{kind:`string`,re:c,first:`'`},{kind:`keyword`,re:/@[\w-]+/y,first:`@`},{kind:`prop`,re:/--?[a-zA-Z][\w-]*(?=\s*:)|[a-zA-Z][\w-]*(?=\s*:[^:{]*[;}\n])/y},{kind:`number`,re:/#[\da-fA-F]{3,8}\b|-?\d*\.?\d+(?:%|[a-zA-Z]{1,4})?/y}],keywords:n(`important inherit initial unset none auto`)},md:{rules:[{kind:`keyword`,re:/#{1,6}\s[^\n]*/y,at:`line`,first:`#`},{kind:`meta`,re:/(?:[-*+]|\d+[.)])(?=\s)/y,at:`line`,first:`-*+0123456789`},{kind:`string`,re:/`[^`\n]*`?/y,first:"`"},{kind:`type`,re:/\*\*[^*\n]+\*\*|__[^_\n]+__/y,first:`*_`},{kind:`attr`,re:/\[[^\]\n]*\]\([^)\n]*\)/y,first:`[`},{kind:`comment`,re:/>[^\n]*/y,at:`line`,first:`>`}]}};function p(e,t){for(let n=t-1;n>=0;n--){let t=e[n];if(t===`
|
|
2
|
+
`)return!0;if(t!==` `&&t!==` `&&t!==`-`)return!1}return!0}function m(e,t){for(let n=t-1;n>=0;n--){let t=e[n];if(t!==` `&&t!==` `)return t===`
|
|
3
|
+
`||t===`|`||t===`;`||t===`&`||t===`(`||t==="`"||t===`$`}return!0}var h=(e,t)=>t===0||/\s/.test(e[t-1]);function g(e,t,n){e.lastIndex=n;let r=e.exec(t);return r&&r[0]?r[0]:void 0}function _(e){let t=[];for(let[n,r]of e.split(`
|
|
4
|
+
`).entries()){if(n&&t.push({text:`
|
|
5
|
+
`}),!r)continue;let e=/^(@@|\+\+\+ |--- |diff |index )/.test(r)?`meta`:r[0]===`+`?`add`:r[0]===`-`?`del`:void 0;t.push(e?{text:r,kind:e}:{text:r})}return t}function v(e){let t=[],n=0;for(;n<e.length;){let r=g(/<!--[\s\S]*?(?:-->|$)/y,e,n);if(r){t.push({text:r,kind:`comment`}),n+=r.length;continue}let i=g(/<\/?[A-Za-z][\w:.-]*|<![A-Za-z]+/y,e,n);if(i)for(t.push({text:i,kind:`tag`}),n+=i.length;n<e.length;){let r=g(/\/?>/y,e,n);if(r){t.push({text:r,kind:`tag`}),n+=r.length;break}let i=g(/\s+/y,e,n),a=i?void 0:g(/[^\s=>/"']+/y,e,n),o=i||a?void 0:g(/"[^"]*"?|'[^']*'?/y,e,n);i?t.push({text:i}):a?t.push({text:a,kind:`attr`}):o?t.push({text:o,kind:`string`}):t.push({text:e[n]}),n+=(i??a??o??e[n]).length}else{let r=g(/[^<]+|</y,e,n);t.push({text:r}),n+=r.length}}return t}function y(e,t){let n=[],i=``,o=(e,t)=>{t?(i&&n.push({text:i}),i=``,n.push({text:e,kind:t})):i+=e},s=0;outer:for(;s<e.length;){let n=e[s];for(let r of t.rules){if(r.first&&!r.first.includes(n)||r.at===`line`&&!p(e,s)||r.at===`word`&&!h(e,s)||r.at===`command`&&!m(e,s))continue;let i=g(r.re,e,s);if(i){o(i,r.kind===`fn`&&t.keywords?.has(i)?`keyword`:r.kind),s+=i.length;continue outer}}if(r(n)){let n=a(e,s),r=t.ci?n.toLowerCase():n,i=e[s+n.length],c;t.keywords?.has(r)?c=`keyword`:t.types?.has(r)?c=`type`:(i===`(`||t.macros&&i===`!`&&e[s+n.length+1]!==`=`)&&(c=`fn`),o(n,c),s+=n.length}else i+=n,s++}return i&&n.push({text:i}),n}function b(e){let t=[];for(let n of e){let e=t.at(-1);e&&!e.kind&&!n.kind?e.text+=n.text:t.push({...n})}return t}function x(e,n){let r=String(e??``);if(!r)return[];let i=t(n);if(i===`diff`)return b(_(r));if(i===`html`)return b(v(r));let a=i?f[i]:void 0;return a?y(r,a):[{text:r}]}export{x as highlight,t as languageOf};
|