@pi-in-go/pigpen-ahp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CREDITS.md +27 -0
- package/LICENSE +23 -0
- package/README.md +148 -0
- package/extensions/ahp/access_options_test.go +116 -0
- package/extensions/ahp/extension.go +54 -0
- package/extensions/ahp/go.mod +12 -0
- package/extensions/ahp/go.sum +2 -0
- package/extensions/ahp/go.work +8 -0
- package/extensions/ahp/internal/channels/chat.go +110 -0
- package/extensions/ahp/internal/channels/root.go +32 -0
- package/extensions/ahp/internal/channels/session.go +128 -0
- package/extensions/ahp/internal/channels/session_test.go +108 -0
- package/extensions/ahp/internal/compose/compose.go +153 -0
- package/extensions/ahp/internal/compose/compose_test.go +120 -0
- package/extensions/ahp/internal/gaps/gaps_test.go +88 -0
- package/extensions/ahp/internal/host/handshake_test.go +206 -0
- package/extensions/ahp/internal/host/host.go +1305 -0
- package/extensions/ahp/internal/host/reconnect_test.go +197 -0
- package/extensions/ahp/internal/host/replaywindow_test.go +31 -0
- package/extensions/ahp/internal/host/schema_test.go +79 -0
- package/extensions/ahp/internal/host/store.go +291 -0
- package/extensions/ahp/internal/host/store_update_test.go +85 -0
- package/extensions/ahp/internal/host/subscriptions_test.go +160 -0
- package/extensions/ahp/internal/host/surface_test.go +95 -0
- package/extensions/ahp/internal/host/workarounds.go +458 -0
- package/extensions/ahp/internal/host/workarounds_test.go +96 -0
- package/extensions/ahp/internal/live/live.go +328 -0
- package/extensions/ahp/internal/live/live_test.go +91 -0
- package/extensions/ahp/internal/live/store.go +113 -0
- package/extensions/ahp/internal/mapper/activity.go +199 -0
- package/extensions/ahp/internal/mapper/activity_test.go +188 -0
- package/extensions/ahp/internal/mapper/bench_test.go +38 -0
- package/extensions/ahp/internal/mapper/event_mapper_test.go +380 -0
- package/extensions/ahp/internal/mapper/fixtures_test.go +387 -0
- package/extensions/ahp/internal/mapper/helpers_test.go +251 -0
- package/extensions/ahp/internal/mapper/js.go +131 -0
- package/extensions/ahp/internal/mapper/mapper.go +600 -0
- package/extensions/ahp/internal/mapper/message_input_test.go +196 -0
- package/extensions/ahp/internal/mapper/messageinput.go +272 -0
- package/extensions/ahp/internal/mapper/schema_helpers_test.go +5 -0
- package/extensions/ahp/internal/mapper/testdata/LICENSE-pi-ahp +21 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/abort.json +711 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/bash-long-output.json +1312 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/compaction.json +6285 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/parallel-tools.json +2622 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/plain-text.json +1856 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/single-tool.json +1128 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/steering.json +3926 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-bash.json +2372 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-edit.json +3983 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-error.json +2850 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-find.json +1162 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-grep.json +2783 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-loop.json +1693 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-ls.json +2427 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-write.json +1271 -0
- package/extensions/ahp/internal/mapper/title.go +34 -0
- package/extensions/ahp/internal/mapper/usermsg.go +87 -0
- package/extensions/ahp/internal/pi/activeturn_test.go +296 -0
- package/extensions/ahp/internal/pi/backend.go +64 -0
- package/extensions/ahp/internal/pi/catalogue.go +370 -0
- package/extensions/ahp/internal/pi/catalogue_test.go +271 -0
- package/extensions/ahp/internal/pi/chatdriver.go +533 -0
- package/extensions/ahp/internal/pi/chatdriver_test.go +416 -0
- package/extensions/ahp/internal/pi/clientactions.go +212 -0
- package/extensions/ahp/internal/pi/clientactions_test.go +273 -0
- package/extensions/ahp/internal/pi/completions.go +230 -0
- package/extensions/ahp/internal/pi/completions_test.go +272 -0
- package/extensions/ahp/internal/pi/config_test.go +269 -0
- package/extensions/ahp/internal/pi/deletesession.go +69 -0
- package/extensions/ahp/internal/pi/deletesession_test.go +153 -0
- package/extensions/ahp/internal/pi/disposal_test.go +362 -0
- package/extensions/ahp/internal/pi/fixture_test.go +308 -0
- package/extensions/ahp/internal/pi/foreign_test.go +122 -0
- package/extensions/ahp/internal/pi/harness_test.go +83 -0
- package/extensions/ahp/internal/pi/history.go +269 -0
- package/extensions/ahp/internal/pi/hydrated_test.go +171 -0
- package/extensions/ahp/internal/pi/hydratedlifecycle_test.go +196 -0
- package/extensions/ahp/internal/pi/hydration_test.go +209 -0
- package/extensions/ahp/internal/pi/hydrator.go +212 -0
- package/extensions/ahp/internal/pi/imageinput.go +224 -0
- package/extensions/ahp/internal/pi/imageinput_test.go +57 -0
- package/extensions/ahp/internal/pi/lifecycle_test.go +381 -0
- package/extensions/ahp/internal/pi/models.go +212 -0
- package/extensions/ahp/internal/pi/models_test.go +128 -0
- package/extensions/ahp/internal/pi/paging.go +95 -0
- package/extensions/ahp/internal/pi/pagingtruncate_test.go +323 -0
- package/extensions/ahp/internal/pi/projecttrust.go +222 -0
- package/extensions/ahp/internal/pi/registry.go +924 -0
- package/extensions/ahp/internal/pi/restart_test.go +185 -0
- package/extensions/ahp/internal/pi/services.go +70 -0
- package/extensions/ahp/internal/pi/sessionconfig.go +64 -0
- package/extensions/ahp/internal/pi/sessionfiles_test.go +74 -0
- package/extensions/ahp/internal/pi/sessionstore.go +83 -0
- package/extensions/ahp/internal/pi/summary_test.go +426 -0
- package/extensions/ahp/internal/pi/workarounds_wire_test.go +208 -0
- package/extensions/ahp/internal/pisession/fromentries_test.go +32 -0
- package/extensions/ahp/internal/pisession/json.go +31 -0
- package/extensions/ahp/internal/pisession/pisession.go +580 -0
- package/extensions/ahp/internal/settings/access.go +63 -0
- package/extensions/ahp/internal/settings/access_test.go +52 -0
- package/extensions/ahp/internal/settings/settings.go +117 -0
- package/extensions/ahp/internal/settings/settings_test.go +104 -0
- package/extensions/ahp/internal/svc/etag_other.go +15 -0
- package/extensions/ahp/internal/svc/etag_unix.go +34 -0
- package/extensions/ahp/internal/svc/glob.go +197 -0
- package/extensions/ahp/internal/svc/mime.go +37 -0
- package/extensions/ahp/internal/svc/paths.go +108 -0
- package/extensions/ahp/internal/svc/pty.go +64 -0
- package/extensions/ahp/internal/svc/pty_darwin.go +42 -0
- package/extensions/ahp/internal/svc/pty_linux.go +36 -0
- package/extensions/ahp/internal/svc/pty_other.go +9 -0
- package/extensions/ahp/internal/svc/pty_test.go +114 -0
- package/extensions/ahp/internal/svc/pty_unix.go +147 -0
- package/extensions/ahp/internal/svc/resource.go +495 -0
- package/extensions/ahp/internal/svc/resource_test.go +501 -0
- package/extensions/ahp/internal/svc/stat_bsd.go +9 -0
- package/extensions/ahp/internal/svc/stat_linux.go +9 -0
- package/extensions/ahp/internal/svc/terminal.go +454 -0
- package/extensions/ahp/internal/svc/terminal_test.go +512 -0
- package/extensions/ahp/internal/svc/watch.go +590 -0
- package/extensions/ahp/internal/svc/watch_test.go +627 -0
- package/extensions/ahp/internal/svc/watchevents_test.go +452 -0
- package/extensions/ahp/internal/svc/watchpolicy.go +81 -0
- package/extensions/ahp/internal/svc/watchpolicy_test.go +151 -0
- package/extensions/ahp/internal/svc/watchracy_test.go +61 -0
- package/extensions/ahp/internal/testkit/schema/LICENSE-agent-host-protocol +21 -0
- package/extensions/ahp/internal/testkit/schema/actions.schema.json +9208 -0
- package/extensions/ahp/internal/testkit/schema/commands.schema.json +10862 -0
- package/extensions/ahp/internal/testkit/schema/errors.schema.json +10928 -0
- package/extensions/ahp/internal/testkit/schema/notifications.schema.json +6667 -0
- package/extensions/ahp/internal/testkit/schema/state.schema.json +6392 -0
- package/extensions/ahp/internal/testkit/schema.go +360 -0
- package/extensions/ahp/internal/testkit/testkit.go +367 -0
- package/extensions/ahp/internal/twin/twin.go +27 -0
- package/extensions/ahp/internal/wire/dispatchable.go +106 -0
- package/extensions/ahp/internal/wire/dispatchable_list.go +15 -0
- package/extensions/ahp/internal/wire/helpers_test.go +5 -0
- package/extensions/ahp/internal/wire/wire.go +218 -0
- package/extensions/ahp/internal/wire/wire_test.go +123 -0
- package/extensions/ahp/internal/ws/helpers_test.go +7 -0
- package/extensions/ahp/internal/ws/vscode_test.go +198 -0
- package/extensions/ahp/internal/ws/ws.go +674 -0
- package/extensions/ahp/internal/ws/ws_test.go +324 -0
- package/extensions/ahp/realpig_test.go +589 -0
- package/extensions/ahp/runtime.go +311 -0
- package/extensions/ahp/runtime_test.go +110 -0
- package/extensions/ahp/testdata/upstream-tests.json +3833 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/LICENSE +21 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/NOTICE-PIGPEN.md +18 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahp/client.go +1011 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahp/error.go +111 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahp/multi_host_state_mirror.go +239 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahp/reducers.go +1939 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahp/transport.go +176 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/actions.generated.go +2447 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/commands.generated.go +1546 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/common.go +205 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/errors.generated.go +65 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/messages.generated.go +138 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/notifications.generated.go +264 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/state.generated.go +6171 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/version.generated.go +31 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/go.mod +3 -0
- package/extensions/ahp/twins_test.go +91 -0
- package/package.json +40 -0
- package/proof/PORT.md +121 -0
- package/proof/mutations.json +152 -0
- package/proof/oracle/LICENSE +21 -0
- package/proof/oracle/README.md +120 -0
- package/proof/oracle/UPSTREAM.md +7 -0
- package/proof/oracle/package.json +68 -0
- package/proof/oracle/src/bin/cli.ts +66 -0
- package/proof/oracle/src/bin/tunnel.ts +128 -0
- package/proof/oracle/src/channels/chat.ts +134 -0
- package/proof/oracle/src/channels/root.ts +39 -0
- package/proof/oracle/src/channels/session.ts +118 -0
- package/proof/oracle/src/channels/terminal.ts +11 -0
- package/proof/oracle/src/core/channels.ts +108 -0
- package/proof/oracle/src/core/client-workarounds.ts +358 -0
- package/proof/oracle/src/core/connection.ts +41 -0
- package/proof/oracle/src/core/host.ts +882 -0
- package/proof/oracle/src/core/sequencer.ts +75 -0
- package/proof/oracle/src/core/state-store.ts +162 -0
- package/proof/oracle/src/core/uri.ts +25 -0
- package/proof/oracle/src/host/direct-settings.ts +110 -0
- package/proof/oracle/src/host/pi-host.ts +220 -0
- package/proof/oracle/src/host/serve.ts +71 -0
- package/proof/oracle/src/host/terminal-service.ts +346 -0
- package/proof/oracle/src/pi/activity.ts +171 -0
- package/proof/oracle/src/pi/changeset-service.ts +646 -0
- package/proof/oracle/src/pi/changeset-uri.ts +64 -0
- package/proof/oracle/src/pi/chat-driver.ts +529 -0
- package/proof/oracle/src/pi/completions.ts +160 -0
- package/proof/oracle/src/pi/delete-session.ts +53 -0
- package/proof/oracle/src/pi/event-mapper.ts +648 -0
- package/proof/oracle/src/pi/git-changes.ts +605 -0
- package/proof/oracle/src/pi/history.ts +305 -0
- package/proof/oracle/src/pi/image-input.ts +56 -0
- package/proof/oracle/src/pi/image-mime.ts +8 -0
- package/proof/oracle/src/pi/in-process-backend.ts +155 -0
- package/proof/oracle/src/pi/message-input.ts +212 -0
- package/proof/oracle/src/pi/models.ts +124 -0
- package/proof/oracle/src/pi/project-trust.ts +66 -0
- package/proof/oracle/src/pi/provider.ts +2 -0
- package/proof/oracle/src/pi/resource-paths.ts +80 -0
- package/proof/oracle/src/pi/resource-service.ts +356 -0
- package/proof/oracle/src/pi/resource-watch-policy.ts +35 -0
- package/proof/oracle/src/pi/resource-watch.ts +357 -0
- package/proof/oracle/src/pi/session-catalogue.ts +318 -0
- package/proof/oracle/src/pi/session-config.ts +84 -0
- package/proof/oracle/src/pi/session-history.ts +75 -0
- package/proof/oracle/src/pi/session-hydrator.ts +219 -0
- package/proof/oracle/src/pi/session-registry.ts +815 -0
- package/proof/oracle/src/pi/session-storage.ts +29 -0
- package/proof/oracle/src/pi/session-title.ts +18 -0
- package/proof/oracle/src/pi/turn-paging.ts +94 -0
- package/proof/oracle/src/pi/user-message.ts +66 -0
- package/proof/oracle/src/protocol/errors.ts +44 -0
- package/proof/oracle/src/protocol/jsonrpc.ts +89 -0
- package/proof/oracle/src/protocol/version.ts +39 -0
- package/proof/oracle/src/transport/websocket.ts +136 -0
- package/proof/oracle/src/tunnel/devtunnel.ts +293 -0
- package/proof/oracle/src/tunnel/discovery.ts +37 -0
- package/proof/oracle/test/active-turn-reconnect.test.ts +274 -0
- package/proof/oracle/test/activity.test.ts +171 -0
- package/proof/oracle/test/changeset-lifecycle.test.serial.ts +303 -0
- package/proof/oracle/test/changeset-uri.test.ts +37 -0
- package/proof/oracle/test/changeset.test.serial.ts +487 -0
- package/proof/oracle/test/chat-driver.test.ts +691 -0
- package/proof/oracle/test/client-actions.test.ts +409 -0
- package/proof/oracle/test/client-workarounds.test.ts +320 -0
- package/proof/oracle/test/completions.test.ts +323 -0
- package/proof/oracle/test/delete-session.test.ts +142 -0
- package/proof/oracle/test/direct-settings.test.ts +81 -0
- package/proof/oracle/test/event-mapper.test.ts +619 -0
- package/proof/oracle/test/fetch-turns.test.ts +207 -0
- package/proof/oracle/test/fixtures/abort.json +711 -0
- package/proof/oracle/test/fixtures/bash-long-output.json +1312 -0
- package/proof/oracle/test/fixtures/compaction.json +6285 -0
- package/proof/oracle/test/fixtures/parallel-tools.json +2622 -0
- package/proof/oracle/test/fixtures/plain-text.json +1856 -0
- package/proof/oracle/test/fixtures/single-tool.json +1128 -0
- package/proof/oracle/test/fixtures/steering.json +3926 -0
- package/proof/oracle/test/fixtures/tool-bash.json +2372 -0
- package/proof/oracle/test/fixtures/tool-edit.json +3983 -0
- package/proof/oracle/test/fixtures/tool-error.json +2850 -0
- package/proof/oracle/test/fixtures/tool-find.json +1162 -0
- package/proof/oracle/test/fixtures/tool-grep.json +2783 -0
- package/proof/oracle/test/fixtures/tool-loop.json +1693 -0
- package/proof/oracle/test/fixtures/tool-ls.json +2427 -0
- package/proof/oracle/test/fixtures/tool-write.json +1271 -0
- package/proof/oracle/test/handshake.test.ts +275 -0
- package/proof/oracle/test/harness.ts +153 -0
- package/proof/oracle/test/hydrated-session-lifecycle.test.ts +211 -0
- package/proof/oracle/test/image-input.test.ts +28 -0
- package/proof/oracle/test/image-session.test.ts +69 -0
- package/proof/oracle/test/live-turn.test.ts +304 -0
- package/proof/oracle/test/mapper-fixtures.test.ts +293 -0
- package/proof/oracle/test/message-input.test.ts +181 -0
- package/proof/oracle/test/model-discovery.test.ts +170 -0
- package/proof/oracle/test/models.test.ts +109 -0
- package/proof/oracle/test/pi-host.test.ts +91 -0
- package/proof/oracle/test/pi-replay.test.ts +141 -0
- package/proof/oracle/test/project-trust.test.ts +125 -0
- package/proof/oracle/test/protocol-surface.test.ts +121 -0
- package/proof/oracle/test/pty.test.ts +100 -0
- package/proof/oracle/test/reconnect.test.ts +389 -0
- package/proof/oracle/test/resource-watch-policy.test.ts +78 -0
- package/proof/oracle/test/resource-watch.test.serial.ts +536 -0
- package/proof/oracle/test/resource.test.ts +499 -0
- package/proof/oracle/test/schema.test.ts +88 -0
- package/proof/oracle/test/session-catalogue.test.ts +314 -0
- package/proof/oracle/test/session-config.test.ts +224 -0
- package/proof/oracle/test/session-disposal.test.ts +364 -0
- package/proof/oracle/test/session-hydration.test.ts +194 -0
- package/proof/oracle/test/session-lifecycle.test.ts +416 -0
- package/proof/oracle/test/session-storage.test.serial.ts +32 -0
- package/proof/oracle/test/session-summary.test.ts +418 -0
- package/proof/oracle/test/subscriptions.test.ts +198 -0
- package/proof/oracle/test/support/assertions.ts +32 -0
- package/proof/oracle/test/support/async.ts +26 -0
- package/proof/oracle/test/support/hydrated-session.ts +205 -0
- package/proof/oracle/test/support/images.ts +8 -0
- package/proof/oracle/test/support/recorded-fixtures.ts +60 -0
- package/proof/oracle/test/support/recorded-scenarios.ts +145 -0
- package/proof/oracle/test/support/replay.ts +194 -0
- package/proof/oracle/test/support/schema.ts +221 -0
- package/proof/oracle/test/support/session-files.ts +12 -0
- package/proof/oracle/test/support/session-storage.ts +22 -0
- package/proof/oracle/test/support/upstream.ts +25 -0
- package/proof/oracle/test/support/watch-events.ts +108 -0
- package/proof/oracle/test/terminal-service.test.ts +495 -0
- package/proof/oracle/test/truncate.test.ts +230 -0
- package/proof/oracle/test/tunnel.test.ts +258 -0
- package/proof/oracle/test/upstream-workarounds.test.ts +37 -0
- package/proof/oracle/test/uri.test.ts +24 -0
- package/proof/oracle/test/watch-events.test.ts +118 -0
- package/proof/oracle/tsconfig.json +32 -0
- package/proof/proof-piglet/piglet.yaml +18 -0
- package/proof/tools/fakellm/go.mod +3 -0
- package/proof/tools/fakellm/main.go +109 -0
- package/proof/tools/gen-dispatchable.py +30 -0
- package/provenance.json +28 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
package pi_test
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"regexp"
|
|
6
|
+
"sort"
|
|
7
|
+
"strings"
|
|
8
|
+
"testing"
|
|
9
|
+
|
|
10
|
+
"github.com/microsoft/agent-host-protocol/clients/go/ahptypes"
|
|
11
|
+
|
|
12
|
+
"github.com/MichaelKinsy/pigpen/ahp/internal/testkit"
|
|
13
|
+
"github.com/MichaelKinsy/pigpen/ahp/internal/twin"
|
|
14
|
+
"github.com/MichaelKinsy/pigpen/ahp/internal/wire"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
// Twins of upstream test/client-actions.test.ts: the client-action policy of this host, seen from
|
|
18
|
+
// a sender and an observer. The fixture is storage-only (no backend).
|
|
19
|
+
|
|
20
|
+
type actionsFixture struct {
|
|
21
|
+
t *testing.T
|
|
22
|
+
h *harness
|
|
23
|
+
client *testkit.Client
|
|
24
|
+
observer *testkit.Client
|
|
25
|
+
workspace string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
func startActions(t *testing.T) *actionsFixture {
|
|
29
|
+
t.Helper()
|
|
30
|
+
workspace := t.TempDir()
|
|
31
|
+
f := &actionsFixture{t: t, workspace: workspace, h: startHarness(t, harnessOptions{workingDir: workspace})}
|
|
32
|
+
f.client, f.observer = testkit.Connect(t, f.h.host), testkit.Connect(t, f.h.host)
|
|
33
|
+
t.Cleanup(f.client.Close)
|
|
34
|
+
t.Cleanup(f.observer.Close)
|
|
35
|
+
f.client.Initialize(nextClientID(), nil)
|
|
36
|
+
f.observer.Initialize(nextClientID(), nil)
|
|
37
|
+
return f
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
func (f *actionsFixture) createChannels() (session, chat string) {
|
|
41
|
+
f.t.Helper()
|
|
42
|
+
id := newID()
|
|
43
|
+
session, chat = wire.SessionURI(id), wire.ChatURI(id)
|
|
44
|
+
f.client.Must("createSession", obj{"channel": session})
|
|
45
|
+
var snapshot ahptypes.SubscribeResult
|
|
46
|
+
for _, c := range []*testkit.Client{f.client, f.observer} {
|
|
47
|
+
snapshot = c.Subscribe(session)
|
|
48
|
+
c.Subscribe(chat)
|
|
49
|
+
}
|
|
50
|
+
// The host attaches the session's backend on its own goroutine, so the subscriber may see lifecycle
|
|
51
|
+
// "creating" and then the host's session/ready action. Wait for that action (the snapshot already
|
|
52
|
+
// says "ready" when the host won the race) so that a state taken before a client action is the state
|
|
53
|
+
// the session settles in. Upstream's harness is ready synchronously in storage-only mode.
|
|
54
|
+
if snapshot.Snapshot == nil || testkit.Normalize(f.t, snapshot.Snapshot.State).(map[string]any)["lifecycle"] != string(ahptypes.SessionLifecycleReady) {
|
|
55
|
+
awaitAction(f.t, f.observer, session, "session/ready")
|
|
56
|
+
}
|
|
57
|
+
return session, chat
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
func (f *actionsFixture) state(channel string) any {
|
|
61
|
+
if wire.IsChatChannel(channel) {
|
|
62
|
+
return f.h.host.Store().Chat(channel)
|
|
63
|
+
}
|
|
64
|
+
return f.h.host.Store().Session(channel)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// rawEnvelope is an action envelope decoded generically: a rejected malformed action cannot be
|
|
68
|
+
// decoded into the typed union, and the echo of one must still be checkable.
|
|
69
|
+
type rawEnvelope struct {
|
|
70
|
+
Channel string
|
|
71
|
+
Action map[string]any
|
|
72
|
+
ServerSeq int64
|
|
73
|
+
Origin *struct{ ClientSeq int64 }
|
|
74
|
+
RejectionReason *string
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
func (f *actionsFixture) nextRaw(c *testkit.Client, channel string, seq int64) rawEnvelope {
|
|
78
|
+
f.t.Helper()
|
|
79
|
+
n, ok := c.Await(func(n testkit.Notification) bool {
|
|
80
|
+
if n.Method != "action" {
|
|
81
|
+
return false
|
|
82
|
+
}
|
|
83
|
+
var e rawEnvelope
|
|
84
|
+
return json.Unmarshal(n.Params, &e) == nil && e.Channel == channel && e.Origin != nil && e.Origin.ClientSeq == seq
|
|
85
|
+
}, testkit.Timeout)
|
|
86
|
+
if !ok {
|
|
87
|
+
f.t.Fatalf("timed out waiting for the echo of clientSeq %d on %s", seq, channel)
|
|
88
|
+
}
|
|
89
|
+
var e rawEnvelope
|
|
90
|
+
_ = json.Unmarshal(n.Params, &e)
|
|
91
|
+
return e
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
func (f *actionsFixture) expectRejected(channel string, action obj, reason string) {
|
|
95
|
+
f.t.Helper()
|
|
96
|
+
before := testkit.Normalize(f.t, f.state(channel))
|
|
97
|
+
seq := f.client.Dispatch(channel, action)
|
|
98
|
+
sender := f.nextRaw(f.client, channel, seq)
|
|
99
|
+
seen := f.nextRaw(f.observer, channel, seq)
|
|
100
|
+
if sender.Action["type"] != action["type"] {
|
|
101
|
+
f.t.Fatalf("%v: echoed action type %v", action["type"], sender.Action["type"])
|
|
102
|
+
}
|
|
103
|
+
if sender.RejectionReason == nil || !regexp.MustCompile(reason).MatchString(*sender.RejectionReason) {
|
|
104
|
+
f.t.Fatalf("%v: rejectionReason = %v, want /%s/", action["type"], sender.RejectionReason, reason)
|
|
105
|
+
}
|
|
106
|
+
if seen.ServerSeq != sender.ServerSeq || seen.RejectionReason == nil || *seen.RejectionReason != *sender.RejectionReason {
|
|
107
|
+
f.t.Fatalf("%v: the observer saw a different envelope: %+v vs %+v", action["type"], seen, sender)
|
|
108
|
+
}
|
|
109
|
+
sameJSON(f.t, seen.Action, sender.Action, "observer's action")
|
|
110
|
+
sameJSON(f.t, f.state(channel), before, "state must not change")
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// The policy cases snapshot a channel's state and compare it after a rejected action, so the fixture must hand
|
|
114
|
+
// back sessions that have finished starting: the host attaches a session's backend on its own goroutine, and a
|
|
115
|
+
// "creating" snapshot followed by the "ready" transition is not a change the rejected action made.
|
|
116
|
+
func TestActionsFixtureHandsBackReadySessions(t *testing.T) {
|
|
117
|
+
f := startActions(t)
|
|
118
|
+
for i := 0; i < 200; i++ {
|
|
119
|
+
session, _ := f.createChannels()
|
|
120
|
+
if got := f.h.host.Store().Session(session).Lifecycle; got != ahptypes.SessionLifecycleReady {
|
|
121
|
+
t.Fatalf("session %d lifecycle = %s, want ready", i, got)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
func TestPiClientActionPolicy(t *testing.T) {
|
|
127
|
+
twin.Run(t, "client-actions", "accounts for every client action on channels this host serves", func(t *testing.T) {
|
|
128
|
+
var actual []string
|
|
129
|
+
for _, typ := range wire.ClientDispatchableTypes() {
|
|
130
|
+
for _, prefix := range []string{"root/", "session/", "chat/", "terminal/"} {
|
|
131
|
+
if strings.HasPrefix(typ, prefix) {
|
|
132
|
+
actual = append(actual, typ)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
expected := []string{
|
|
137
|
+
"root/configChanged", "session/titleChanged", "session/activeClientSet", "session/activeClientRemoved",
|
|
138
|
+
"session/workingDirectorySet", "session/workingDirectoryRemoved", "session/workingDirectoryReplaced",
|
|
139
|
+
"session/customizationToggled", "session/mcpServerStartRequested", "session/mcpServerStopRequested",
|
|
140
|
+
"session/isReadChanged", "session/isArchivedChanged", "session/configChanged",
|
|
141
|
+
"chat/turnStarted", "chat/turnResume", "chat/toolCallConfirmed", "chat/toolCallComplete",
|
|
142
|
+
"chat/toolCallResultConfirmed", "chat/toolCallContentChanged", "chat/turnCancelled",
|
|
143
|
+
"chat/workingDirectorySet", "chat/workingDirectoryRemoved", "chat/pendingMessageSet",
|
|
144
|
+
"chat/pendingMessageRemoved", "chat/queuedMessagesReordered", "chat/draftChanged",
|
|
145
|
+
"chat/inputAnswerChanged", "chat/inputCompleted", "chat/truncated",
|
|
146
|
+
"terminal/input", "terminal/resized", "terminal/claimed", "terminal/titleChanged", "terminal/cleared",
|
|
147
|
+
// Client-dispatchable in the pinned spec commit but absent from the npm 0.9.0 package
|
|
148
|
+
// upstream tested against (see PORT.md); this host refuses both.
|
|
149
|
+
"chat/isArchivedChanged", "session/mcpServerBackgroundRequested",
|
|
150
|
+
}
|
|
151
|
+
sort.Strings(expected)
|
|
152
|
+
sameJSON(t, actual, expected, "client-dispatchable actions on served channels")
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
twin.Run(t, "client-actions", "rejects actions addressed to the wrong channel kind", func(t *testing.T) {
|
|
156
|
+
f := startActions(t)
|
|
157
|
+
session, chat := f.createChannels()
|
|
158
|
+
f.expectRejected(session, obj{"type": "chat/draftChanged", "draft": userMessage("wrong channel")}, `does not belong on a session channel`)
|
|
159
|
+
f.expectRejected(chat, obj{"type": "session/titleChanged", "title": "wrong channel"}, `does not belong on a chat channel`)
|
|
160
|
+
f.expectRejected(chat, obj{"type": "terminal/cleared"}, `does not belong on a chat channel`)
|
|
161
|
+
f.expectRejected(session, obj{"type": "root/configChanged", "config": obj{}}, `does not belong on a session channel`)
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
twin.Run(t, "client-actions", "rejects capabilities pi does not implement", func(t *testing.T) {
|
|
165
|
+
f := startActions(t)
|
|
166
|
+
session, chat := f.createChannels()
|
|
167
|
+
wd := "file://" + f.workspace
|
|
168
|
+
for _, a := range []obj{
|
|
169
|
+
{"type": "session/workingDirectorySet", "directory": "file:///tmp/other"},
|
|
170
|
+
{"type": "session/workingDirectoryRemoved", "directory": wd},
|
|
171
|
+
{"type": "session/workingDirectoryReplaced", "directory": wd, "replacement": "file:///tmp/other"},
|
|
172
|
+
} {
|
|
173
|
+
f.expectRejected(session, a, `changing working directories`)
|
|
174
|
+
}
|
|
175
|
+
for _, a := range []obj{
|
|
176
|
+
{"type": "chat/workingDirectorySet", "directory": "file:///tmp/other"},
|
|
177
|
+
{"type": "chat/workingDirectoryRemoved", "directory": wd},
|
|
178
|
+
} {
|
|
179
|
+
f.expectRejected(chat, a, `changing working directories`)
|
|
180
|
+
}
|
|
181
|
+
f.expectRejected(session, obj{"type": "session/customizationToggled", "id": "plugin", "enablement": []any{}}, `customizations`)
|
|
182
|
+
for _, typ := range []string{"session/mcpServerStartRequested", "session/mcpServerStopRequested", "session/mcpServerBackgroundRequested"} {
|
|
183
|
+
f.expectRejected(session, obj{"type": typ, "id": "mcp"}, `MCP servers`)
|
|
184
|
+
}
|
|
185
|
+
f.expectRejected(session, obj{"type": "session/isReadChanged", "isRead": false}, `read or archive state`)
|
|
186
|
+
f.expectRejected(session, obj{"type": "session/isArchivedChanged", "isArchived": true}, `read or archive state`)
|
|
187
|
+
f.expectRejected(chat, obj{"type": "chat/isArchivedChanged", "isArchived": true}, `read or archive state`)
|
|
188
|
+
f.expectRejected(session, obj{"type": "session/configChanged", "config": obj{"probe": true}}, `no mutable configuration`)
|
|
189
|
+
for _, a := range []obj{
|
|
190
|
+
{"type": "chat/toolCallConfirmed", "turnId": "turn", "toolCallId": "tool", "approved": false, "reason": "denied"},
|
|
191
|
+
{"type": "chat/toolCallComplete", "turnId": "turn", "toolCallId": "tool", "result": obj{"success": false, "pastTenseMessage": "Failed"}},
|
|
192
|
+
{"type": "chat/toolCallResultConfirmed", "turnId": "turn", "toolCallId": "tool", "approved": true},
|
|
193
|
+
{"type": "chat/toolCallContentChanged", "turnId": "turn", "toolCallId": "tool", "content": []any{}},
|
|
194
|
+
} {
|
|
195
|
+
f.expectRejected(chat, a, `client tool execution or confirmation`)
|
|
196
|
+
}
|
|
197
|
+
f.expectRejected(chat, obj{"type": "chat/turnResume", "turnId": "turn"}, `cannot resume an errored turn`)
|
|
198
|
+
f.expectRejected(chat, obj{"type": "chat/inputAnswerChanged", "requestId": "input", "questionId": "question"}, `interactive input requests`)
|
|
199
|
+
f.expectRejected(chat, obj{"type": "chat/inputCompleted", "requestId": "input", "response": "cancel"}, `interactive input requests`)
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
twin.Run(t, "client-actions", "rejects malformed supported actions without mutating state", func(t *testing.T) {
|
|
203
|
+
f := startActions(t)
|
|
204
|
+
session, chat := f.createChannels()
|
|
205
|
+
f.expectRejected(session, obj{"type": "session/titleChanged", "title": 42}, `title must be a string`)
|
|
206
|
+
f.expectRejected(chat, obj{"type": "chat/turnStarted", "turnId": "malformed", "startedAt": "2025-01-01T00:00:00.000Z",
|
|
207
|
+
"message": obj{"text": 42, "origin": obj{"kind": "user"}}}, `requires text and an origin`)
|
|
208
|
+
f.expectRejected(chat, obj{"type": "chat/queuedMessagesReordered", "order": []any{42}}, `order must be an array of ids`)
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
twin.Run(t, "client-actions", "validates turn and user-message invariants", func(t *testing.T) {
|
|
212
|
+
f := startActions(t)
|
|
213
|
+
_, chat := f.createChannels()
|
|
214
|
+
agent := userMessage("not from the user")
|
|
215
|
+
agent["origin"] = obj{"kind": "agent"}
|
|
216
|
+
at := "2025-01-01T00:00:00.000Z"
|
|
217
|
+
f.expectRejected(chat, obj{"type": "chat/turnStarted", "turnId": "agent-turn", "startedAt": at, "message": agent}, `only start a turn with a user message`)
|
|
218
|
+
f.expectRejected(chat, obj{"type": "chat/pendingMessageSet", "kind": "queued", "id": "agent-queue", "message": agent}, `only queue a user message`)
|
|
219
|
+
f.expectRejected(chat, obj{"type": "chat/draftChanged", "draft": agent}, `only draft a user message`)
|
|
220
|
+
withContext := userMessage("with context")
|
|
221
|
+
withContext["attachments"] = []any{obj{"type": "simple", "label": "context"}}
|
|
222
|
+
f.expectRejected(chat, obj{"type": "chat/turnStarted", "turnId": "attachment-turn", "startedAt": at, "message": withContext}, `requires modelRepresentation`)
|
|
223
|
+
customAgent := userMessage("custom agent")
|
|
224
|
+
customAgent["agent"] = obj{"uri": "agent:/fixture"}
|
|
225
|
+
f.expectRejected(chat, obj{"type": "chat/draftChanged", "draft": customAgent}, `custom agents`)
|
|
226
|
+
f.expectRejected(chat, obj{"type": "chat/turnStarted", "turnId": "queued-turn", "startedAt": at, "message": userMessage("queued"), "queuedMessageId": "client-owned"}, `Only the host can start a queued message`)
|
|
227
|
+
f.expectRejected(chat, obj{"type": "chat/turnCancelled", "turnId": "absent", "duration": 0}, `No matching active turn`)
|
|
228
|
+
f.expectRejected(chat, obj{"type": "chat/pendingMessageRemoved", "kind": "queued", "id": "absent"}, `No matching queued message`)
|
|
229
|
+
f.h.host.DispatchServerAction(chat, ahptypes.StateAction{Value: &ahptypes.ChatPendingMessageSetAction{
|
|
230
|
+
Type: ahptypes.ActionTypeChatPendingMessageSet, Kind: ahptypes.PendingMessageKindSteering, Id: "steering",
|
|
231
|
+
Message: ahptypes.Message{Text: "steer", Origin: ahptypes.MessageOrigin{Kind: ahptypes.MessageKindUser}},
|
|
232
|
+
}})
|
|
233
|
+
f.expectRejected(chat, obj{"type": "chat/pendingMessageRemoved", "kind": "steering", "id": "steering"}, `cannot be withdrawn`)
|
|
234
|
+
f.h.host.DispatchServerAction(chat, ahptypes.StateAction{Value: &ahptypes.ChatTurnStartedAction{
|
|
235
|
+
Type: ahptypes.ActionTypeChatTurnStarted, TurnId: "active", StartedAt: at,
|
|
236
|
+
Message: ahptypes.Message{Text: "active", Origin: ahptypes.MessageOrigin{Kind: ahptypes.MessageKindUser}},
|
|
237
|
+
}})
|
|
238
|
+
f.expectRejected(chat, obj{"type": "chat/turnStarted", "turnId": "second", "startedAt": at, "message": userMessage("second")}, `A turn is already active`)
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
twin.Run(t, "client-actions", "keeps state-only draft and queued-message actions", func(t *testing.T) {
|
|
242
|
+
f := startActions(t)
|
|
243
|
+
_, chat := f.createChannels()
|
|
244
|
+
accepted := func(action obj) {
|
|
245
|
+
t.Helper()
|
|
246
|
+
seq := f.client.Dispatch(chat, action)
|
|
247
|
+
if env := f.client.NextEnvelope(chat, seq); env.RejectionReason != nil {
|
|
248
|
+
t.Fatalf("%v rejected: %s", action["type"], *env.RejectionReason)
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
accepted(obj{"type": "chat/draftChanged", "draft": userMessage("draft")})
|
|
252
|
+
if got := f.h.host.Store().Chat(chat).Draft; got == nil || got.Text != "draft" {
|
|
253
|
+
t.Fatalf("draft = %+v", got)
|
|
254
|
+
}
|
|
255
|
+
for _, id := range []string{"first", "second"} {
|
|
256
|
+
f.h.host.DispatchServerAction(chat, ahptypes.StateAction{Value: &ahptypes.ChatPendingMessageSetAction{
|
|
257
|
+
Type: ahptypes.ActionTypeChatPendingMessageSet, Kind: ahptypes.PendingMessageKindQueued, Id: id,
|
|
258
|
+
Message: ahptypes.Message{Text: id, Origin: ahptypes.MessageOrigin{Kind: ahptypes.MessageKindUser}},
|
|
259
|
+
}})
|
|
260
|
+
}
|
|
261
|
+
ids := func() []string {
|
|
262
|
+
var out []string
|
|
263
|
+
for _, m := range f.h.host.Store().Chat(chat).QueuedMessages {
|
|
264
|
+
out = append(out, m.Id)
|
|
265
|
+
}
|
|
266
|
+
return out
|
|
267
|
+
}
|
|
268
|
+
accepted(obj{"type": "chat/queuedMessagesReordered", "order": []string{"second", "first"}})
|
|
269
|
+
sameJSON(t, ids(), []string{"second", "first"}, "queued order")
|
|
270
|
+
accepted(obj{"type": "chat/pendingMessageRemoved", "kind": "queued", "id": "first"})
|
|
271
|
+
sameJSON(t, ids(), []string{"second"}, "queued after removal")
|
|
272
|
+
})
|
|
273
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
package pi
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"io/fs"
|
|
6
|
+
"net/url"
|
|
7
|
+
"path/filepath"
|
|
8
|
+
"sort"
|
|
9
|
+
"strings"
|
|
10
|
+
"unicode/utf16"
|
|
11
|
+
|
|
12
|
+
"github.com/microsoft/agent-host-protocol/clients/go/ahptypes"
|
|
13
|
+
|
|
14
|
+
"github.com/MichaelKinsy/pigpen/ahp/internal/mapper"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
// Inline completions for a message being composed (port of src/pi/completions.ts).
|
|
18
|
+
//
|
|
19
|
+
// The protocol makes CompletionItem.attachment required, which decides what this feature is: an
|
|
20
|
+
// attachment picker. "@"-mentions fit; Pi's slash commands do not, because they attach nothing.
|
|
21
|
+
|
|
22
|
+
// MentionTrigger is the character that opens the file picker.
|
|
23
|
+
const MentionTrigger = "@"
|
|
24
|
+
|
|
25
|
+
const (
|
|
26
|
+
maxCompletionItems = 50
|
|
27
|
+
// maxScannedEntries bounds the directory walk of a huge working directory.
|
|
28
|
+
maxScannedEntries = 10_000
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
// skippedDirectories are never descended into. The list is deliberately narrow: build output such
|
|
32
|
+
// as dist/ and build/ is often exactly what a user wants to mention.
|
|
33
|
+
var skippedDirectories = map[string]bool{".git": true, "node_modules": true}
|
|
34
|
+
|
|
35
|
+
// score ranks a relative path against a lower-cased query: an exact file name beats a prefix beats
|
|
36
|
+
// a substring of the name beats a substring of the path; 0 is no match.
|
|
37
|
+
func score(relativePath, query string) int {
|
|
38
|
+
if query == "" {
|
|
39
|
+
return 1
|
|
40
|
+
}
|
|
41
|
+
path := strings.ToLower(relativePath)
|
|
42
|
+
name := path[strings.LastIndexByte(path, '/')+1:]
|
|
43
|
+
switch {
|
|
44
|
+
case name == query:
|
|
45
|
+
return 4
|
|
46
|
+
case strings.HasPrefix(name, query):
|
|
47
|
+
return 3
|
|
48
|
+
case strings.Contains(name, query):
|
|
49
|
+
return 2
|
|
50
|
+
case strings.Contains(path, query):
|
|
51
|
+
return 1
|
|
52
|
+
}
|
|
53
|
+
return 0
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Mention is the "@" token the cursor sits in; offsets are UTF-16 code units, the unit clients
|
|
57
|
+
// count text in.
|
|
58
|
+
type Mention struct {
|
|
59
|
+
Start, End int
|
|
60
|
+
Query string
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
func isSpaceUnit(u uint16) bool {
|
|
64
|
+
return u < 0xd800 && isJSSpaceRune(rune(u)) || u >= 0xe000 && isJSSpaceRune(rune(u))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
func isJSSpaceRune(r rune) bool { return mapperIsSpace(r) }
|
|
68
|
+
|
|
69
|
+
// FindMention finds the mention the cursor is in, if any. An "@" only opens one at the start of
|
|
70
|
+
// the text or after whitespace (otherwise every email address and decorator opens the picker), and
|
|
71
|
+
// the cursor must not have moved past the mention's end.
|
|
72
|
+
func FindMention(text string, offset int) (Mention, bool) {
|
|
73
|
+
units := utf16.Encode([]rune(text))
|
|
74
|
+
clamped := offset
|
|
75
|
+
if clamped < 0 {
|
|
76
|
+
clamped = 0
|
|
77
|
+
}
|
|
78
|
+
if clamped > len(units) {
|
|
79
|
+
clamped = len(units)
|
|
80
|
+
}
|
|
81
|
+
for index := clamped; index > 0; index-- {
|
|
82
|
+
char := units[index-1]
|
|
83
|
+
if isSpaceUnit(char) {
|
|
84
|
+
return Mention{}, false
|
|
85
|
+
}
|
|
86
|
+
if char == '@' {
|
|
87
|
+
if index >= 2 && !isSpaceUnit(units[index-2]) {
|
|
88
|
+
return Mention{}, false
|
|
89
|
+
}
|
|
90
|
+
return Mention{Start: index - 1, End: clamped, Query: string(utf16.Decode(units[index:clamped]))}, true
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return Mention{}, false
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// CompletionServiceOptions configure a [CompletionService].
|
|
97
|
+
type CompletionServiceOptions struct {
|
|
98
|
+
// WorkingDirectoryFor is the directory a chat's mentions resolve against ("" when unknown).
|
|
99
|
+
WorkingDirectoryFor func(chat string) string
|
|
100
|
+
// MaxItems caps the result (default 50).
|
|
101
|
+
MaxItems int
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// CompletionService implements host.CompletionHandler.
|
|
105
|
+
type CompletionService struct{ opts CompletionServiceOptions }
|
|
106
|
+
|
|
107
|
+
// NewCompletionService creates the service.
|
|
108
|
+
func NewCompletionService(opts CompletionServiceOptions) *CompletionService {
|
|
109
|
+
return &CompletionService{opts: opts}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
type candidate struct {
|
|
113
|
+
absolute, relative string
|
|
114
|
+
rank int
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// localeLess approximates String.localeCompare for path tie-breaking: case-insensitive first, then
|
|
118
|
+
// case-sensitive (ICU collation differs from this on punctuation; see PORT.md).
|
|
119
|
+
func localeLess(a, b string) bool {
|
|
120
|
+
la, lb := strings.ToLower(a), strings.ToLower(b)
|
|
121
|
+
if la != lb {
|
|
122
|
+
return la < lb
|
|
123
|
+
}
|
|
124
|
+
return a < b
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Complete implements host.CompletionHandler.
|
|
128
|
+
func (s *CompletionService) Complete(_ context.Context, p ahptypes.CompletionsParams) (ahptypes.CompletionsResult, error) {
|
|
129
|
+
empty := ahptypes.CompletionsResult{Items: []ahptypes.CompletionItem{}}
|
|
130
|
+
if p.Kind != ahptypes.CompletionItemKindUserMessage {
|
|
131
|
+
return empty, nil
|
|
132
|
+
}
|
|
133
|
+
workingDirectory := s.opts.WorkingDirectoryFor(p.Channel)
|
|
134
|
+
if workingDirectory == "" {
|
|
135
|
+
return empty, nil
|
|
136
|
+
}
|
|
137
|
+
mention, ok := FindMention(p.Text, int(p.Offset))
|
|
138
|
+
if !ok {
|
|
139
|
+
return empty, nil
|
|
140
|
+
}
|
|
141
|
+
query := strings.ToLower(strings.TrimPrefix(strings.ReplaceAll(mention.Query, `\`, "/"), "./"))
|
|
142
|
+
|
|
143
|
+
var candidates []candidate
|
|
144
|
+
scanned := 0
|
|
145
|
+
errScanLimit := fs.SkipAll
|
|
146
|
+
err := filepath.WalkDir(workingDirectory, func(path string, d fs.DirEntry, err error) error {
|
|
147
|
+
if err != nil {
|
|
148
|
+
if path == workingDirectory {
|
|
149
|
+
return err
|
|
150
|
+
}
|
|
151
|
+
return nil // an unreadable entry is skipped, not fatal
|
|
152
|
+
}
|
|
153
|
+
if path == workingDirectory {
|
|
154
|
+
return nil
|
|
155
|
+
}
|
|
156
|
+
name := d.Name()
|
|
157
|
+
// Like a glob, dot-entries are neither matched nor descended into.
|
|
158
|
+
if strings.HasPrefix(name, ".") {
|
|
159
|
+
if d.IsDir() {
|
|
160
|
+
return fs.SkipDir
|
|
161
|
+
}
|
|
162
|
+
return nil
|
|
163
|
+
}
|
|
164
|
+
if d.IsDir() && skippedDirectories[name] {
|
|
165
|
+
return fs.SkipDir
|
|
166
|
+
}
|
|
167
|
+
scanned++
|
|
168
|
+
if scanned > maxScannedEntries {
|
|
169
|
+
return errScanLimit
|
|
170
|
+
}
|
|
171
|
+
if d.IsDir() {
|
|
172
|
+
return nil
|
|
173
|
+
}
|
|
174
|
+
// Files and symbolic links are offered; sockets, devices and the like are not.
|
|
175
|
+
if !d.Type().IsRegular() && d.Type()&fs.ModeSymlink == 0 {
|
|
176
|
+
return nil
|
|
177
|
+
}
|
|
178
|
+
rel, err := filepath.Rel(workingDirectory, path)
|
|
179
|
+
if err != nil {
|
|
180
|
+
return nil
|
|
181
|
+
}
|
|
182
|
+
rel = filepath.ToSlash(rel)
|
|
183
|
+
if rank := score(rel, query); rank > 0 {
|
|
184
|
+
candidates = append(candidates, candidate{absolute: path, relative: rel, rank: rank})
|
|
185
|
+
}
|
|
186
|
+
return nil
|
|
187
|
+
})
|
|
188
|
+
if err != nil {
|
|
189
|
+
return empty, nil
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
sort.SliceStable(candidates, func(i, j int) bool {
|
|
193
|
+
if candidates[i].rank != candidates[j].rank {
|
|
194
|
+
return candidates[i].rank > candidates[j].rank
|
|
195
|
+
}
|
|
196
|
+
return localeLess(candidates[i].relative, candidates[j].relative)
|
|
197
|
+
})
|
|
198
|
+
max := s.opts.MaxItems
|
|
199
|
+
if max <= 0 {
|
|
200
|
+
max = maxCompletionItems
|
|
201
|
+
}
|
|
202
|
+
if len(candidates) > max {
|
|
203
|
+
candidates = candidates[:max]
|
|
204
|
+
}
|
|
205
|
+
start, end := int64(mention.Start), int64(mention.End)
|
|
206
|
+
items := make([]ahptypes.CompletionItem, 0, len(candidates))
|
|
207
|
+
for _, c := range candidates {
|
|
208
|
+
kind := "document"
|
|
209
|
+
items = append(items, ahptypes.CompletionItem{
|
|
210
|
+
InsertText: MentionTrigger + c.relative, RangeStart: &start, RangeEnd: &end,
|
|
211
|
+
Attachment: ahptypes.MessageAttachment{Value: &ahptypes.MessageResourceAttachment{
|
|
212
|
+
Type: ahptypes.MessageAttachmentKindResource, Label: filepathBase(c.relative), DisplayKind: &kind, Uri: fileURL(c.absolute),
|
|
213
|
+
}},
|
|
214
|
+
})
|
|
215
|
+
}
|
|
216
|
+
return ahptypes.CompletionsResult{Items: items}, nil
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
func filepathBase(rel string) string { return rel[strings.LastIndexByte(rel, '/')+1:] }
|
|
220
|
+
|
|
221
|
+
// fileURL is url.pathToFileURL(path).toString().
|
|
222
|
+
func fileURL(path string) string {
|
|
223
|
+
slashed := filepath.ToSlash(path)
|
|
224
|
+
if !strings.HasPrefix(slashed, "/") {
|
|
225
|
+
slashed = "/" + slashed
|
|
226
|
+
}
|
|
227
|
+
return (&url.URL{Scheme: "file", Path: slashed}).String()
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
func mapperIsSpace(r rune) bool { return mapper.IsJSSpace(r) }
|