From 1aec2a5cd94df9ad2df53a8615fcd2790ba3cd65 Mon Sep 17 00:00:00 2001 From: ayattara Date: Fri, 21 Aug 2026 00:05:16 +0000 Subject: [PATCH 1/2] feat: select notes agent profiles directly --- CHANGELOG.md | 11 +++- Makefile | 2 +- README.md | 63 ++++++++-------------- manifest.yaml | 16 +++--- server/manifest_test.go | 11 ++-- server/plugin.go | 117 ++++++++++++++++++++-------------------- server/plugin_test.go | 106 ++++++++++++++++++------------------ ui/bundle.js | 30 ++++++----- ui/bundle.test.mjs | 17 +++--- 9 files changed, 187 insertions(+), 186 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4294abd..1e64e4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## [0.5.0] - 2026-08-21 + +### Changed + +- Enhance with AI now selects a platform `agent_profile` directly in Notes + settings, replacing the custom `utility_agent` picker for new + configurations. +- Guided setup now distinguishes missing and ineligible agent profiles, while + preserving disabled and unconfigured legacy Utility Agent guidance. + ## [0.4.0] - 2026-08-18 ### Changed @@ -78,4 +88,3 @@ - ci: tidy go.mod to promote direct grpc dependency (0ed27c4) - qa: handle list toolbar on blank notes (a1c18cd) - feat: markdown toolbar, AI-assisted proofreading, and a fixed-size note modal (4c84eb9) - diff --git a/Makefile b/Makefile index 81d25a0..f8b4b92 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ .PHONY: build run test fmt vet package package-host clean BIN := bin/kandev-plugin-notes -VERSION := 0.4.0 +VERSION := 0.5.0 STAGE := .build/stage PKG_OUT := kandev-plugin-notes-$(VERSION).tar.gz diff --git a/README.md b/README.md index 565b2d6..2ce957d 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ editor and optional AI-assisted proofreading. them off via the checklist syntax instead. - **Enhance with AI** — a button next to the toolbar/editor sends the note's - current markdown to your configured utility agent to proofread grammar, + current markdown to your configured agent profile to proofread grammar, spelling, and clarity. The result is shown as a preview with **Accept**/**Discard** before it ever replaces your note — nothing is overwritten automatically. See "Notes are private to you" below for the @@ -99,7 +99,7 @@ each see their own note; nobody else can read yours, and no task's agent can read or write it. **The one exception is the "Enhance with AI" button.** Clicking it sends the -note's current markdown to the utility agent configured for this plugin +note's current markdown to the agent profile configured for this plugin (**Settings > Plugins > Notes**) via a one-shot completion (`capabilities.agent_invoke` / `Host.InvokeUtilityAgent`) — that content leaves the "nobody else can read it" boundary for that one request. See @@ -112,47 +112,28 @@ description or say it in chat. This is a scratchpad, not a shared field. ## Setting up Enhance with AI -"Enhance with AI" needs **two separate settings**, both satisfied, before it -can run: - -1. **Select an agent for this plugin** — Settings > Plugins > Notes, - `config_schema.utility_agent`. This is what tells the plugin which - utility agent to ask. -2. **Enable that agent, with a model** — Settings > Utility Agents. Selecting - an agent in step 1 does not enable it; a newly-added utility agent starts - disabled with no model chosen. - -Both steps are required because **a disabled utility agent is usable by -kandev's own built-in features (e.g. task-create prompt enhancement) but not -by any plugin**, including this one. Kandev's own prompt-enhancement path -does not check `Enabled`; this plugin's request goes through -`Host.InvokeUtilityAgent`, which does. That asymmetry is host behavior this -plugin cannot change — clicking Enhance with an agent selected-but-disabled -fails exactly like having no agent selected at all, and the two failures now -say so explicitly rather than both pointing back at Settings > Plugins > Notes: +"Enhance with AI" needs one setting before it can run: + +1. **Select an agent profile for this plugin** — Settings > Plugins > Notes, + `config_schema.agent_profile`. Pick a profile that can run utility + completions; this is the profile that proofreads the note. + +The picker lists platform agent profiles directly. You do not need to create, +enable, or bind a custom **Utility Agent** for new Notes installations. | Situation | Message points you to | | --- | --- | -| No agent ever selected | Settings > Plugins > Notes | -| Selected agent was since deleted | Settings > Plugins > Notes | -| Selected agent exists but is disabled | **Settings > Utility Agents** ("Enable the agent") | -| Selected and enabled, but no model / agent profile bound | **Settings > Utility Agents** ("Finish setting up the agent") | -| Any other setup problem the plugin can't identify | no page named; the host's own wording is quoted instead | -| A real execution failure (the agent ran and failed) | no settings link — try again | - -For the first four, **Dismiss** is joined by a second action button that -jumps straight to the right page for that cause, so there's no need to guess -which setting is missing. - -The fourth row is the one most people hit, because **every built-in utility -agent ships with no model bound**: completing steps 1 and 2 above still leaves -it unconfigured. It is called out separately from "disabled" on purpose — -both are fixed on the same page but by different controls, and being told to -enable an agent you just enabled is the dead end this plugin exists to avoid. - -The fifth row is the honest fallback: a `FailedPrecondition` this plugin does -not recognize (for example after a host rephrase). It names no page, because -any page it named would be a guess, and quotes the host's own wording instead. +| No profile selected | Settings > Plugins > Notes | +| Selected profile was deleted | Settings > Plugins > Notes | +| Selected profile cannot run utility completions | Settings > Plugins > Notes | +| A legacy Utility Agent is disabled | **Settings > Utility Agents** ("Enable the agent") | +| A legacy Utility Agent has no bound profile | **Settings > Utility Agents** ("Finish setting up the agent") | +| A real execution failure | no settings link — try again | + +For a recognized setup problem, **Dismiss** is joined by an action that opens +the relevant settings page. The two Utility Agents messages only apply to +older configurations retained for compatibility; selecting an agent profile +in Notes is the supported setup for new installations. ## Install @@ -168,7 +149,7 @@ curl -F "package=@kandev-plugin-notes-.tar.gz" \ Sideloaded plugins register disabled/unverified; enable it in **Settings > Plugins**. Reinstalling the same version returns 409 — bump the version in `manifest.yaml` (and `Makefile`) first. To use "Enhance with AI", -see "Setting up Enhance with AI" above — it's a two-step setup, not one. +select an agent profile as described in "Setting up Enhance with AI" above. ## Development diff --git a/manifest.yaml b/manifest.yaml index 4ad6401..068c554 100644 --- a/manifest.yaml +++ b/manifest.yaml @@ -5,7 +5,7 @@ # (/api/plugins//...). Keep the three in sync when bumping version. id: "kandev-plugin-notes" api_version: 1 -version: "0.4.0" +version: "0.5.0" display_name: "Notes" description: "A private, per-user scratchpad note on any task or workspace: a dockview/mobile panel, a sidebar workspace-notes button, markdown editing with a formatting toolbar, AI-assisted proofreading with guided setup, and a kanban card shortcut." author: "yattdev" @@ -29,7 +29,7 @@ runtime: # per-user browser-reachable storage (host.storage) for the note itself, plus # agent_invoke so the "Enhance with AI" button can run a one-shot proofreading # completion via Host.InvokeUtilityAgent (see server/plugin.go and the -# `utility_agent` config property below). +# `agent_profile` config property below). capabilities: user_state: true agent_invoke: true @@ -40,18 +40,18 @@ capabilities: # and returns the improved markdown for the UI's preview/Accept/Discard flow. webhooks: - key: "enhance" - description: "Proofread and improve the current note's markdown via the configured utility agent" + description: "Proofread and improve the current note's markdown via the configured agent profile" method: "POST" -# config_schema: agent_invoke requires a utility_agent picker (rendered at -# Settings > Plugins > Notes) so the operator selects which agent runs the -# enhance completion. Calling Host.InvokeUtilityAgent without a valid -# selection here returns a distinguishable FailedPrecondition (see +# config_schema: agent_invoke requires an agent_profile picker (rendered at +# Settings > Plugins > Notes) so the operator selects the platform profile +# that runs the enhance completion. Calling Host.InvokeUtilityAgent without a +# valid selection returns a distinguishable FailedPrecondition (see # server/plugin.go), surfaced by the UI as a non-fatal message. config_schema: type: object properties: - utility_agent: { type: string, format: utility-agent, title: "Utility Agent", description: "Agent used to proofread notes for the Enhance with AI button" } + agent_profile: { type: string, format: agent-profile, title: "Agent profile", description: "Agent profile used to proofread notes for the Enhance with AI button" } # ui.bundle points at the hand-written, no-build ES module frontend half. ui: diff --git a/server/manifest_test.go b/server/manifest_test.go index 778fd9d..48946fe 100644 --- a/server/manifest_test.go +++ b/server/manifest_test.go @@ -118,7 +118,7 @@ func TestManifestCapabilities_UserStateAndAgentInvoke(t *testing.T) { require.Equal(t, map[string]any{"user_state": true, "agent_invoke": true}, m.Capabilities) } -func TestManifest_DeclaresEnhanceWebhookAndUtilityAgentConfig(t *testing.T) { +func TestManifest_DeclaresEnhanceWebhookAndAgentProfileConfig(t *testing.T) { m := loadManifest(t) require.Len(t, m.Webhooks, 1) webhook, ok := m.Webhooks[0].(map[string]any) @@ -129,10 +129,11 @@ func TestManifest_DeclaresEnhanceWebhookAndUtilityAgentConfig(t *testing.T) { require.NotEmpty(t, m.ConfigSchema) properties, ok := m.ConfigSchema["properties"].(map[string]any) require.True(t, ok, "config_schema.properties should decode as a map") - utilityAgent, ok := properties["utility_agent"].(map[string]any) - require.True(t, ok, "config_schema.properties.utility_agent should decode as a map") - require.Equal(t, "string", utilityAgent["type"]) - require.Equal(t, "utility-agent", utilityAgent["format"]) + agentProfile, ok := properties["agent_profile"].(map[string]any) + require.True(t, ok, "config_schema.properties.agent_profile should decode as a map") + require.Equal(t, "string", agentProfile["type"]) + require.Equal(t, "agent-profile", agentProfile["format"]) + require.NotContains(t, properties, "utility_agent") } func TestManifestUI_BundlePathHasLeadingSlash(t *testing.T) { diff --git a/server/plugin.go b/server/plugin.go index 97f35d5..fb8d45b 100644 --- a/server/plugin.go +++ b/server/plugin.go @@ -5,7 +5,7 @@ // manifest.yaml's capabilities.user_state and ui/bundle.js) — this backend // never reads or writes note content directly. Its one job is the // "Enhance with AI" feature: relay the note's current markdown to the -// operator-configured utility agent (capabilities.agent_invoke, +// operator-configured agent profile (capabilities.agent_invoke, // Host.InvokeUtilityAgent) via a single webhook and return the proofread // markdown, so the frontend never needs its own LLM credentials. OnEvent // stays the UnimplementedPlugin no-op — this plugin subscribes to no events. @@ -58,12 +58,16 @@ type enhanceResponseBody struct { type enhanceErrorCode string const ( - // enhanceErrorCodeAgentUnset: the plugin has no utility agent selected + // enhanceErrorCodeAgentUnset: the plugin has no agent profile selected // at all (Settings > Plugins > Notes was never used). enhanceErrorCodeAgentUnset enhanceErrorCode = "agent_unset" - // enhanceErrorCodeAgentMissing: the selected agent id no longer exists + // enhanceErrorCodeAgentMissing: the selected agent profile no longer exists // (deleted after selection). enhanceErrorCodeAgentMissing enhanceErrorCode = "agent_missing" + // enhanceErrorCodeAgentIneligible: the selected profile cannot run a + // utility completion. Selecting an eligible profile in this plugin's + // settings is the only remedy. + enhanceErrorCodeAgentIneligible enhanceErrorCode = "agent_ineligible" // enhanceErrorCodeAgentDisabled: the selected agent exists but is // disabled — a different fix (Settings > Utility Agents), not a // reselection, per the ADR 0048 Enabled asymmetry this plugin cannot @@ -78,11 +82,6 @@ const ( // state every built-in utility agent ships in, so it is what a user hits // immediately after completing the README's two documented steps. enhanceErrorCodeAgentUnconfiguredProfile enhanceErrorCode = "agent_unconfigured_profile" - // enhanceErrorCodeAgentUnavailable: a FailedPrecondition whose wording - // matched none of the above — the host may have rephrased its message. - // Detail still carries that raw wording verbatim so the user sees real - // information rather than a guessed instruction. - enhanceErrorCodeAgentUnavailable enhanceErrorCode = "agent_unavailable" ) // enhanceErrorMessages pairs each code with the one correct remedy. Keep @@ -90,52 +89,58 @@ const ( // composes an error message inline, so unset/missing and disabled can never // be swapped at a call site. var enhanceErrorMessages = map[enhanceErrorCode]string{ - enhanceErrorCodeAgentUnset: "No utility agent is configured for this plugin — configure one in Settings > Plugins > Notes.", - enhanceErrorCodeAgentMissing: "The utility agent configured for this plugin no longer exists — choose another one in Settings > Plugins > Notes.", + enhanceErrorCodeAgentUnset: "No agent profile is configured for this plugin — choose one in Settings > Plugins > Notes.", + enhanceErrorCodeAgentMissing: "The agent profile configured for this plugin no longer exists — choose another one in Settings > Plugins > Notes.", + enhanceErrorCodeAgentIneligible: "The agent profile configured for this plugin is not eligible for utility execution — choose an eligible profile in Settings > Plugins > Notes.", enhanceErrorCodeAgentDisabled: "The utility agent configured for this plugin is disabled — enable it (with a model) in Settings > Utility Agents.", enhanceErrorCodeAgentUnconfiguredProfile: "The utility agent configured for this plugin has no model or agent profile bound — finish setting it up in Settings > Utility Agents.", - enhanceErrorCodeAgentUnavailable: "The configured utility agent is unavailable.", } -// enhanceErrorMessage resolves the user-facing message for a code. A -// classified cause names the one page that fixes it. agent_unavailable is by -// definition a FailedPrecondition this plugin could NOT classify, so there is -// no page it can name without guessing — naming one anyway is how a user who -// has already done that step gets sent back to it. It therefore surfaces the -// host's own wording instead of a remedy that may not apply, which is the -// degradation this classifier was designed for: a missing button, not a wrong -// instruction. -func enhanceErrorMessage(code enhanceErrorCode, detail string) string { - message := enhanceErrorMessages[code] - if code == enhanceErrorCodeAgentUnavailable && detail != "" { - return message + " The host reported: " + detail - } - return message +// enhanceErrorMessage resolves the user-facing message for a recognized +// configuration error. Unknown gRPC errors are execution failures, not setup +// states, and therefore never reach this function. +func enhanceErrorMessage(code enhanceErrorCode) string { + return enhanceErrorMessages[code] } -// classifyUtilityAgentError maps each of host_utility.go's four -// FailedPrecondition wordings ("no utility agent configured for this plugin", -// "configured utility agent %q not found", "configured utility agent %q is -// disabled", "configured utility agent %q has no usable agent profile") to a -// stable code, kept as its own function (rather than inlined at the call -// site) so the mapping is unit-testable in isolation and has exactly one -// home. Substring matching is coupled to the host's current wording — a -// rephrase degrades to enhanceErrorCodeAgentUnavailable rather than -// misclassifying, since Detail (the raw message) is always preserved -// alongside it. -func classifyUtilityAgentError(message string) enhanceErrorCode { +// classifyAgentProfileError maps the host's direct-profile FailedPrecondition +// messages to stable codes. It also retains the two legacy utility-agent +// messages that have distinct Utility Agents remedies for existing configs. +// The direct messages are intentionally exact (apart from the quoted ID): an +// unfamiliar host message must remain an execution failure instead of being +// guessed as a settings problem. +func classifyAgentProfileError(message string) (enhanceErrorCode, bool) { switch { - case strings.Contains(message, "no utility agent configured"): - return enhanceErrorCodeAgentUnset - case strings.Contains(message, "not found"): - return enhanceErrorCodeAgentMissing - case strings.Contains(message, "is disabled"): - return enhanceErrorCodeAgentDisabled - case strings.Contains(message, "no usable agent profile"): - return enhanceErrorCodeAgentUnconfiguredProfile + case message == "no agent profile configured for this plugin": + return enhanceErrorCodeAgentUnset, true + case isQuotedConfigurationError(message, "configured agent profile ", " not found"): + return enhanceErrorCodeAgentMissing, true + case isQuotedConfigurationError(message, "configured agent profile ", " is not eligible for utility execution"): + return enhanceErrorCodeAgentIneligible, true + case isQuotedConfigurationError(message, "configured utility agent ", " is disabled"): + return enhanceErrorCodeAgentDisabled, true + case isQuotedConfigurationError(message, "configured utility agent ", " has no usable agent profile"): + return enhanceErrorCodeAgentUnconfiguredProfile, true + case message == "no utility agent configured for this plugin": + // Old hosts used this before the agent_profile config field. The new + // Notes settings page is still the right place to select a profile. + return enhanceErrorCodeAgentUnset, true + case isQuotedConfigurationError(message, "configured utility agent ", " not found"): + return enhanceErrorCodeAgentMissing, true default: - return enhanceErrorCodeAgentUnavailable + return "", false + } +} + +// isQuotedConfigurationError recognizes the host's formatted %q ID messages +// without loosening the classifier to unrelated prose that merely shares a +// suffix such as "not found". +func isQuotedConfigurationError(message, prefix, suffix string) bool { + if !strings.HasPrefix(message, prefix) || !strings.HasSuffix(message, suffix) { + return false } + id := strings.TrimSuffix(strings.TrimPrefix(message, prefix), suffix) + return len(id) >= 2 && id[0] == '"' && id[len(id)-1] == '"' } // enhanceErrorBody is the JSON body returned on a handled failure (missing @@ -187,19 +192,17 @@ func (p *notesPlugin) HandleWebhook(ctx context.Context, req *pluginsdk.WebhookR improved, err := host.InvokeUtilityAgent(ctx, fmt.Sprintf(enhancePromptTemplate, body.Content)) if err != nil { if status.Code(err) == codes.FailedPrecondition { - // No utility agent configured, or the configured one was - // deleted/disabled — a distinguishable, non-fatal condition per - // ADR 0048, not an internal error. classifyUtilityAgentError - // turns the host's raw gRPC message into a stable code (C1) so - // the UI can point at the correct settings page (C2) instead of - // this one message covering unset/missing/disabled alike. + // A known configuration failure is non-fatal and lets the UI point + // at the one setting that fixes it. Other FailedPreconditions can + // describe a failed invocation, so they remain execution errors. rawMessage := status.Convert(err).Message() - code := classifyUtilityAgentError(rawMessage) - return jsonResponse(http.StatusPreconditionFailed, enhanceErrorBody{ - Error: enhanceErrorMessage(code, rawMessage), - Code: code, - Detail: rawMessage, - }) + if code, ok := classifyAgentProfileError(rawMessage); ok { + return jsonResponse(http.StatusPreconditionFailed, enhanceErrorBody{ + Error: enhanceErrorMessage(code), + Code: code, + Detail: rawMessage, + }) + } } return jsonErrorResponse(http.StatusBadGateway, "AI enhancement failed") } diff --git a/server/plugin_test.go b/server/plugin_test.go index 121c095..7d892f4 100644 --- a/server/plugin_test.go +++ b/server/plugin_test.go @@ -7,6 +7,7 @@ package main import ( "context" "encoding/json" + "net/http" "testing" "github.com/kandev/kandev/pkg/pluginsdk" @@ -170,10 +171,10 @@ func TestHandleWebhook_Enhance_NoHost_ReturnsServiceUnavailable(t *testing.T) { require.Equal(t, int32(503), resp.Status) } -func TestHandleWebhook_Enhance_NoUtilityAgentConfigured_ReturnsPreconditionFailed(t *testing.T) { +func TestHandleWebhook_Enhance_NoAgentProfileConfigured_ReturnsPreconditionFailed(t *testing.T) { p := ¬esPlugin{} p.SetHost(&fakeHost{invokeUtilityAgent: func(context.Context, string) (string, error) { - return "", status.Error(codes.FailedPrecondition, "no utility agent configured for this plugin") + return "", status.Error(codes.FailedPrecondition, "no agent profile configured for this plugin") }}) body, err := json.Marshal(map[string]string{"content": "hello"}) @@ -191,7 +192,7 @@ func TestHandleWebhook_Enhance_NoUtilityAgentConfigured_ReturnsPreconditionFaile require.NoError(t, json.Unmarshal(resp.Body, &out)) require.NotEmpty(t, out.Error) require.Equal(t, enhanceErrorCodeAgentUnset, out.Code) - require.Equal(t, "no utility agent configured for this plugin", out.Detail) + require.Equal(t, "no agent profile configured for this plugin", out.Detail) } func TestHandleWebhook_Enhance_OtherAgentError_ReturnsBadGateway(t *testing.T) { @@ -220,9 +221,9 @@ func TestHandleWebhook_Enhance_OtherAgentError_ReturnsBadGateway(t *testing.T) { require.Equal(t, "AI enhancement failed", out.Error) } -// TestHandleWebhook_Enhance_ClassifiesEachFailedPreconditionWording is C1/C7: -// each of host_utility.go's classified wordings, plus unclassified ones, maps -// to a code with the raw message preserved verbatim as Detail. +// TestHandleWebhook_Enhance_ClassifiesKnownConfigurationFailures confirms the +// direct agent-profile contract and legacy utility-agent compatibility both +// retain their distinct, guided 412 responses. func TestHandleWebhook_Enhance_ClassifiesEachFailedPreconditionWording(t *testing.T) { tests := []struct { name string @@ -230,32 +231,27 @@ func TestHandleWebhook_Enhance_ClassifiesEachFailedPreconditionWording(t *testin wantCode enhanceErrorCode }{ { - name: "unset", - hostMessage: "no utility agent configured for this plugin", + name: "direct profile unset", + hostMessage: "no agent profile configured for this plugin", wantCode: enhanceErrorCodeAgentUnset, }, { - name: "missing", - hostMessage: `configured utility agent "builtin-enhance-prompt" not found`, + name: "direct profile missing", + hostMessage: `configured agent profile "profile-1" not found`, wantCode: enhanceErrorCodeAgentMissing, }, { - name: "disabled", - hostMessage: `configured utility agent "builtin-enhance-prompt" is disabled`, - wantCode: enhanceErrorCodeAgentDisabled, + name: "direct profile ineligible", + hostMessage: `configured agent profile "profile-1" is not eligible for utility execution`, + wantCode: enhanceErrorCodeAgentIneligible, }, { - name: "unrecognized wording degrades to unavailable, not a wrong instruction", - hostMessage: "utility agent invocation is temporarily throttled", - wantCode: enhanceErrorCodeAgentUnavailable, + name: "legacy disabled utility agent", + hostMessage: `configured utility agent "builtin-enhance-prompt" is disabled`, + wantCode: enhanceErrorCodeAgentDisabled, }, { - // host_utility.go:84 — reachable by following the README's own - // two-step setup: agent selected AND enabled, but the profile - // binding left at its shipped default (every builtin utility - // agent starts with an empty agent_profile_id). Confirmed live - // against a real host during QA. - name: "enabled agent with no bound profile", + name: "legacy utility agent with no bound profile", hostMessage: `configured utility agent "builtin-enhance-prompt" has no usable agent profile`, wantCode: enhanceErrorCodeAgentUnconfiguredProfile, }, @@ -283,57 +279,63 @@ func TestHandleWebhook_Enhance_ClassifiesEachFailedPreconditionWording(t *testin require.NoError(t, json.Unmarshal(resp.Body, &out)) require.Equal(t, tt.wantCode, out.Code) require.Equal(t, tt.hostMessage, out.Detail) - require.Equal(t, enhanceErrorMessage(tt.wantCode, tt.hostMessage), out.Error) - - // An unclassified cause must not prescribe a settings page: the - // user may have already completed the step it would name. It - // carries the host's own wording instead. - if tt.wantCode == enhanceErrorCodeAgentUnavailable { - require.NotContains(t, out.Error, "Settings >") - require.Contains(t, out.Error, tt.hostMessage) - } + require.Equal(t, enhanceErrorMessage(tt.wantCode), out.Error) }) } } -// TestEnhanceErrorMessage_UnavailableNamesNoPage pins the rule directly: every -// classified code names exactly one remedy page, and agent_unavailable names -// none. Without this, a later edit could quietly reintroduce a wrong-page -// instruction for a cause the plugin cannot identify. -func TestEnhanceErrorMessage_UnavailableNamesNoPage(t *testing.T) { +func TestEnhanceErrorMessage_ClassifiedErrorsNameTheirRemedy(t *testing.T) { for _, code := range []enhanceErrorCode{ enhanceErrorCodeAgentUnset, enhanceErrorCodeAgentMissing, + enhanceErrorCodeAgentIneligible, enhanceErrorCodeAgentDisabled, enhanceErrorCodeAgentUnconfiguredProfile, } { - require.Contains(t, enhanceErrorMessage(code, "raw detail"), "Settings >", + require.Contains(t, enhanceErrorMessage(code), "Settings >", "classified code %q must name its remedy page", code) } +} - unavailable := enhanceErrorMessage(enhanceErrorCodeAgentUnavailable, "raw detail") - require.NotContains(t, unavailable, "Settings >") - require.Contains(t, unavailable, "raw detail") +func TestHandleWebhook_Enhance_UnrecognizedFailedPreconditionIsExecutionFailure(t *testing.T) { + p := ¬esPlugin{} + p.SetHost(&fakeHost{invokeUtilityAgent: func(context.Context, string) (string, error) { + return "", status.Error(codes.FailedPrecondition, "utility execution is temporarily throttled") + }}) - // No detail to pass through: still no invented page. - require.NotContains(t, enhanceErrorMessage(enhanceErrorCodeAgentUnavailable, ""), "Settings >") + body, err := json.Marshal(map[string]string{"content": "hello"}) + require.NoError(t, err) + resp, err := p.HandleWebhook(context.Background(), &pluginsdk.WebhookRequest{WebhookKey: "enhance", Method: http.MethodPost, Body: body}) + require.NoError(t, err) + require.Equal(t, int32(http.StatusBadGateway), resp.Status) + + var out enhanceErrorBody + require.NoError(t, json.Unmarshal(resp.Body, &out)) + require.Equal(t, "AI enhancement failed", out.Error) + require.Empty(t, out.Code) + require.Empty(t, out.Detail) } -// TestClassifyUtilityAgentError_TableDriven exercises classifyUtilityAgentError -// directly, isolated from HandleWebhook and the gRPC status plumbing. -func TestClassifyUtilityAgentError_TableDriven(t *testing.T) { +// TestClassifyAgentProfileError_TableDriven exercises the exact host-message +// classifier without the gRPC transport. +func TestClassifyAgentProfileError_TableDriven(t *testing.T) { tests := []struct { message string want enhanceErrorCode + ok bool }{ - {"no utility agent configured for this plugin", enhanceErrorCodeAgentUnset}, - {`configured utility agent "x" not found`, enhanceErrorCodeAgentMissing}, - {`configured utility agent "x" is disabled`, enhanceErrorCodeAgentDisabled}, - {`configured utility agent "x" has no usable agent profile`, enhanceErrorCodeAgentUnconfiguredProfile}, - {"", enhanceErrorCodeAgentUnavailable}, - {"something else entirely", enhanceErrorCodeAgentUnavailable}, + {"no agent profile configured for this plugin", enhanceErrorCodeAgentUnset, true}, + {`configured agent profile "x" not found`, enhanceErrorCodeAgentMissing, true}, + {`configured agent profile "x" is not eligible for utility execution`, enhanceErrorCodeAgentIneligible, true}, + {`configured utility agent "x" is disabled`, enhanceErrorCodeAgentDisabled, true}, + {`configured utility agent "x" has no usable agent profile`, enhanceErrorCodeAgentUnconfiguredProfile, true}, + {`configured agent profile profile-1 not found`, "", false}, + {"", "", false}, + {"something else entirely", "", false}, } for _, tt := range tests { - require.Equal(t, tt.want, classifyUtilityAgentError(tt.message), "message: %q", tt.message) + got, ok := classifyAgentProfileError(tt.message) + require.Equal(t, tt.ok, ok, "message: %q", tt.message) + require.Equal(t, tt.want, got, "message: %q", tt.message) } } diff --git a/ui/bundle.js b/ui/bundle.js index fd7e1c8..9cb031a 100644 --- a/ui/bundle.js +++ b/ui/bundle.js @@ -45,7 +45,7 @@ // document shared by everyone on the task; the empty-state copy below says // so explicitly. AI enhance is the one documented exception to "the agent // cannot read your note": clicking it sends the note's current markdown to -// the operator-configured utility agent (README's Privacy section explains +// the operator-configured agent profile (README's Privacy section explains // this trade-off). // DEFAULT_SCOPE is the store/cache default when no scope is given, keeping @@ -290,16 +290,16 @@ export async function enhanceNote(host, content) { } if (!response.ok) { - // 412 is this webhook's distinguishable "no utility agent configured" + // 412 is this webhook's distinguishable agent-profile configuration // signal (server/plugin.go, mapped from gRPC FailedPrecondition per // ADR 0048) — surfaced as a clear, non-fatal message rather than a // generic failure. `code`/`detail` (C1) let the UI point at the right - // settings page instead of one message covering unset/missing/disabled + // settings page instead of one message covering unset/missing/ineligible // alike; an older server that omits them (C5) leaves both undefined and // the caller falls back to the plain message with no action button. const notConfigured = response.status === 412; const message = notConfigured - ? (data && data.error) || "No utility agent is configured for this plugin yet." + ? (data && data.error) || "No agent profile is configured for this plugin yet." : (data && data.error) || `Could not enhance this note (status ${response.status}).`; const error = new Error(message); error.notConfigured = notConfigured; @@ -359,24 +359,26 @@ export function enhancePreviewReducer(state, action) { // enhanceErrorAction (C2/C4) maps an enhance failure's `code` to the guided // setup action NotesEditor's error branch renders beside Dismiss: which -// settings page fixes *this* cause, in its own words. "unset"/"missing" both -// land on the Notes plugin page (pick or re-pick an agent); "disabled" and +// settings page fixes *this* cause, in its own words. "unset"/"missing" and +// "ineligible" all land on the Notes plugin page (pick an eligible profile); +// the legacy "disabled" and // "unconfigured_profile" land on Utility Agents instead — a different page, -// because picking an agent there again would not fix either one (see -// server/plugin.go's classifyUtilityAgentError comment for the host-side half +// because picking a profile there again would not fix either one (see +// server/plugin.go's classifyAgentProfileError comment for the host-side half // of this split). The two Utility Agents causes keep separate labels because // they are separate controls on that page: flipping Enabled, versus binding a // model/profile. Telling someone to "enable" an agent they just enabled is // the dead end this whole mapping exists to remove. // A pure function (no host, no React) so C7's code -> action mapping is -// testable directly; returns null for an absent/unrecognized code (C5: an -// older server that omits `code`, or "agent_unavailable" — a cause the plugin -// could not identify, where the message alone is what's known). +// testable directly; returns null for an absent or unrecognized code, such as +// an older server that omits `code`. export function enhanceErrorAction(code) { switch (code) { case "agent_unset": case "agent_missing": - return { label: "Choose an agent", href: "/settings/plugins/kandev-plugin-notes" }; + return { label: "Choose an agent profile", href: "/settings/plugins/kandev-plugin-notes" }; + case "agent_ineligible": + return { label: "Choose an eligible profile", href: "/settings/plugins/kandev-plugin-notes" }; case "agent_disabled": return { label: "Enable the agent", href: "/settings/utility-agents" }; case "agent_unconfigured_profile": @@ -1253,7 +1255,7 @@ function NotesEditor({ host, scope = DEFAULT_SCOPE, scopeId, taskId, surfaceId, disabled: isEnhancing || !snapshot.value, "aria-busy": isEnhancing, "data-testid": "notes-enhance-button", - title: "Enhance with AI — proofread this note with your configured utility agent", + title: "Enhance with AI — proofread this note with your configured agent profile", }, isEnhancing ? h(ui.Spinner, { className: "h-4 w-4" }) @@ -1365,7 +1367,7 @@ function NotesEditor({ host, scope = DEFAULT_SCOPE, scopeId, taskId, surfaceId, h( "p", { style: { color: "var(--muted-foreground)", fontSize: "0.8rem", marginBottom: "0.5rem" } }, - "Private to you — only you can see this note. Using \u201cEnhance with AI\u201d sends its content to your configured utility agent.", + "Private to you — only you can see this note. Using \u201cEnhance with AI\u201d sends its content to your configured agent profile.", ), h( "div", diff --git a/ui/bundle.test.mjs b/ui/bundle.test.mjs index 4fc85aa..7f54ed1 100644 --- a/ui/bundle.test.mjs +++ b/ui/bundle.test.mjs @@ -1138,14 +1138,14 @@ test("enhanceNote posts the note content to webhooks/enhance and returns the imp test("enhanceNote maps a 412 response to a distinguishable notConfigured error", async () => { const host = fakeApiHost(async () => - fakeJsonResponse(412, { error: "no utility agent is configured for this plugin" }), + fakeJsonResponse(412, { error: "no agent profile is configured for this plugin" }), ); await assert.rejects( () => enhanceNote(host, "raw markdown"), (error) => { assert.equal(error.notConfigured, true); - assert.match(error.message, /utility agent/); + assert.match(error.message, /agent profile/); return true; }, ); @@ -1190,13 +1190,17 @@ test("C5: enhanceNote leaves code/detail undefined when an older server's 412 bo // --- C2/C4/C5: enhanceErrorAction maps a code to its one correct remedy --- -test("C2: enhanceErrorAction sends agent_unset and agent_missing to the Notes plugin settings page", () => { +test("C2: enhanceErrorAction sends direct profile setup errors to the Notes plugin settings page", () => { assert.deepEqual(enhanceErrorAction("agent_unset"), { - label: "Choose an agent", + label: "Choose an agent profile", href: "/settings/plugins/kandev-plugin-notes", }); assert.deepEqual(enhanceErrorAction("agent_missing"), { - label: "Choose an agent", + label: "Choose an agent profile", + href: "/settings/plugins/kandev-plugin-notes", + }); + assert.deepEqual(enhanceErrorAction("agent_ineligible"), { + label: "Choose an eligible profile", href: "/settings/plugins/kandev-plugin-notes", }); }); @@ -1217,8 +1221,7 @@ test("agent_unconfigured_profile gets its own action on Utility Agents, distinct assert.notEqual(action.href, enhanceErrorAction("agent_unset").href); }); -test("C5: enhanceErrorAction returns null for agent_unavailable, an unrecognized code, and a missing code", () => { - assert.equal(enhanceErrorAction("agent_unavailable"), null); +test("C5: enhanceErrorAction returns null for an unrecognized or missing code", () => { assert.equal(enhanceErrorAction("something_new_the_server_added"), null); assert.equal(enhanceErrorAction(undefined), null); }); From 80ba6f1205f84b5f2e1afa581dadafc54324f58b Mon Sep 17 00:00:00 2001 From: ayattara Date: Fri, 21 Aug 2026 00:11:09 +0000 Subject: [PATCH 2/2] docs: finalize notes agent-profile release docs --- CHANGELOG.md | 2 +- README.md | 26 -------------------------- 2 files changed, 1 insertion(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e64e4d..2943b9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [0.5.0] - 2026-08-21 +## [0.5.0] - 2026-08-20 ### Changed diff --git a/README.md b/README.md index 2ce957d..3f67273 100644 --- a/README.md +++ b/README.md @@ -177,29 +177,3 @@ and the [manifest reference](https://github.com/kdlbs/kandev/blob/main/docs/publ ## License MIT — see [LICENSE](LICENSE). - - -The Kandev plugin SDK (`pkg/pluginsdk`) is not yet published as a standalone Go -module, so `go.mod` uses a local `replace` that expects the Kandev monorepo as a -**sibling checkout**: - -```text -some-dir/ -├── kandev/ # https://github.com/kdlbs/kandev — Go module at apps/backend/ -└── kandev-plugin-notes/ # this repo -``` - -```sh -make build # host binary -make test # Go + JS unit tests -make vet -make package-host # host-platform package — fastest local loop -make package # all five platforms -``` - -Start with the [plugin authoring guide](https://github.com/kdlbs/kandev/blob/main/docs/public/plugins-authoring.md) -and the [manifest reference](https://github.com/kdlbs/kandev/blob/main/docs/public/plugins-manifest.md). - -## License - -MIT — see [LICENSE](LICENSE).