Add scratchpad_delete MCP tool
Mirrors the existing scratchpad_* tools end-to-end: catalog schema, dispatch, ToolHost.ScratchpadDelete, and a host method that delegates to scratchpad.Store.Delete and fires scratchpadsChanged() on success so the sidebar refreshes. Missing-pad errors surface rather than being masked. Resolves the [MCP SCRATCHPAD DELETE] TODO item.
This commit is contained in:
@@ -6,6 +6,10 @@ loosely follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- MCP clients can now call `scratchpad_delete` with a scratchpad name
|
||||
to remove a shared project scratchpad.
|
||||
|
||||
### Fixed
|
||||
- Sidebar timer indicators now repaint as their visible countdown
|
||||
value changes, so labels progress from minutes to seconds without
|
||||
|
||||
69
TODO.md
69
TODO.md
@@ -1,5 +1,74 @@
|
||||
- [ ] We should deduplicate /r/n newlines or /n newlines to save tokens on mcp responses for terminal reads.
|
||||
- [ ] Codex idle detection seems to trigger too soon, see below [CODEX IDLE]
|
||||
- [ ] Issue with mcp timing out [MCP TIMEOUT]
|
||||
- [ ] I have to run the close action twice on opencode for it to actually close the tab.
|
||||
- [ ] When opening a codex sub agent, the message gets input to the field, but the message is never submitted.
|
||||
- This appears to be inconsistent. Sometimes it works, sometimes it doesn't. Might be because of popups on codex sub agents?
|
||||
- Question: when it fails, is a Codex startup popup visible (trust/workspace, auth/model selection, permissions), or is the normal composer focused?
|
||||
- Question: if the message is sitting in the composer, does pressing Enter once manually submit it, or does something else need to be dismissed first?
|
||||
- Question: does this happen with short one-line prompts as well as long/multiline sub-agent instructions?
|
||||
|
||||
|
||||
|
||||
# [CODEX IDLE]
|
||||
Crunched for 1m 57s │
|
||||
│ Agent Tree
|
||||
❯ codex-implement is idle. Read your own pane for the [sub-agent:codex-implement] reply. │ ─────────────────────────
|
||||
│ ▎ ○ claude ⏱ 59m
|
||||
Called patterm, searched memories (ctrl+o to expand) │ ● codex-implement-2
|
||||
│
|
||||
● Bash(git -C /home/harry/Dev/popdog/.worktrees/harvester-issue-136-kick-segment-repair log -3 --oneline && git -C /home/harry/Dev/popdog/.worktrees/harvester-issue-13…) │ Issue #136 plan drafted
|
||||
⎿ 9604013 fix(youtube): finalize segments on ghost-cleanup path (#120) │ in scratchpad;
|
||||
6da9125 fix(kick): bump KickCategoryAuditor MaxPerCycle to 500 (#135) │ verification sub-agent
|
||||
db149e6 fix(twitch): enqueue stream segment repairs (#134) │
|
||||
⎿ Allowed by auto mode classifier │ Scratchpads
|
||||
│ ─────────────────────────
|
||||
● No commits in the worktree. Let me check the codex-implement pane. │ 0027-review
|
||||
│ 569-state
|
||||
Called patterm (ctrl+o to expand) │ db-infra-options
|
||||
│ db-perf-batching-plan
|
||||
● Codex exited mid-update prompting "Please restart Codex." Let me spawn a fresh one — the update is now installed. │ e2e-canonicalisation-ru…
|
||||
│ issue-136-kick-segment-…
|
||||
Called patterm 3 times (ctrl+o to expand) │ pg-stat-optimization-de…
|
||||
│ pg-stat-statements-find…
|
||||
● Fresh codex-implement-2 spawned. Waiting. │ pr-569-stream-category-…
|
||||
│ stream-category-cutover…
|
||||
✻ Sautéed for 55s │
|
||||
│
|
||||
❯ codex-implement-2 is idle. Read your own pane for the [sub-agent:codex-implement-2] reply. │
|
||||
│
|
||||
● Bash(git log -3 --oneline && echo "---STATUS---" && git status -s && echo "---BRANCH---" && git branch -vv && echo "---REMOTE---" && git ls-remote origin fix/kick-se…) │
|
||||
⎿ 9604013 fix(youtube): finalize segments on ghost-cleanup path (#120) │
|
||||
6da9125 fix(kick): bump KickCategoryAuditor MaxPerCycle to 500 (#135) │
|
||||
db149e6 fix(twitch): enqueue stream segment repairs (#134) │
|
||||
… +44 lines (ctrl+o to expand) │
|
||||
⎿ Allowed by auto mode classifier │
|
||||
⎿ Interrupted · What should Claude do instead? │
|
||||
│
|
||||
❯ it's still running. │
|
||||
│
|
||||
● Got it — re-scheduling the wait. │
|
||||
│
|
||||
Called patterm (ctrl+o to expand) │
|
||||
│
|
||||
● Waiting for codex-implement-2 to finish. │
|
||||
│
|
||||
✻ Sautéed for 12s │
|
||||
│
|
||||
7 tasks (5 done, 1 in progress, 1 open) │
|
||||
◼ Spawn codex sub-agent to implement plan │
|
||||
◻ Open PR for issue 136 │
|
||||
✔ Read GitHub issue 136 and gather reference code │
|
||||
✔ Set up worktree for issue 136 implementation │
|
||||
✔ Draft implementation plan │
|
||||
… +2 completed │
|
||||
|
||||
|
||||
# [MCP TIMEOUT]
|
||||
⚙ patterm_send_input [key=enter, kind=key, process_id=p_a6726d, submit=false, tail_mode=stream, text=, wait_ms=1000] │
|
||||
│
|
||||
⚙ patterm_wait_for_pattern [pattern=Findings|No findings|No issues|Residual risk, process_id=p_a6726d, scope=scrollback, timeout_seconds=300] │
|
||||
MCP error -32001: Request timed out │
|
||||
│
|
||||
⚙ patterm_get_process_status [process_id=p_a6726d] │
|
||||
MCP error -32001: Request timed out │
|
||||
|
||||
@@ -832,6 +832,14 @@ func (h *toolHost) ScratchpadAppend(name, content string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *toolHost) ScratchpadDelete(name string) error {
|
||||
err := h.pads.Delete(name)
|
||||
if err == nil && h.scratch != nil {
|
||||
h.scratch.scratchpadsChanged()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *toolHost) WhoAmI(callerID string) mcp.WhoAmI {
|
||||
w := mcp.WhoAmI{
|
||||
ProcessID: callerID,
|
||||
@@ -1091,7 +1099,7 @@ func availableToolsForRole(role mcp.CallerRole) []string {
|
||||
"send_input", "send_message", "request_human_attention",
|
||||
"timer_wait", "timer_set", "timer_fire_when_idle_any", "timer_fire_when_idle_all",
|
||||
"timer_cancel", "timer_pause", "timer_resume", "timer_list",
|
||||
"scratchpad_list", "scratchpad_read", "scratchpad_write", "scratchpad_append",
|
||||
"scratchpad_list", "scratchpad_read", "scratchpad_write", "scratchpad_append", "scratchpad_delete",
|
||||
"whoami", "help",
|
||||
}
|
||||
if role == mcp.RoleOrchestrator {
|
||||
@@ -1146,8 +1154,8 @@ func helpFor(topic string) mcp.HelpResponse {
|
||||
case "scratchpads":
|
||||
return mcp.HelpResponse{
|
||||
Topic: "scratchpads",
|
||||
Content: "Project-scoped markdown files. Read returns content + revision; pass that back as expected_revision on write to get last-write-wins-with-detection. Append is unconditional.",
|
||||
RelatedTools: []string{"scratchpad_list", "scratchpad_read", "scratchpad_write", "scratchpad_append"},
|
||||
Content: "Project-scoped markdown files. Read returns content + revision; pass that back as expected_revision on write to get last-write-wins-with-detection. Append is unconditional; delete removes a pad by name.",
|
||||
RelatedTools: []string{"scratchpad_list", "scratchpad_read", "scratchpad_write", "scratchpad_append", "scratchpad_delete"},
|
||||
}
|
||||
case "timers":
|
||||
return mcp.HelpResponse{
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/hjbdev/patterm/internal/preset"
|
||||
"github.com/hjbdev/patterm/internal/scratchpad"
|
||||
)
|
||||
|
||||
@@ -95,3 +97,41 @@ func TestDeletingLastFocusedScratchpadFocusesRunningChild(t *testing.T) {
|
||||
t.Fatalf("focusedID = %q, want pid", st.focusedID)
|
||||
}
|
||||
}
|
||||
|
||||
type scratchpadChangeRecorder struct {
|
||||
count int
|
||||
}
|
||||
|
||||
func (r *scratchpadChangeRecorder) scratchpadsChanged() {
|
||||
r.count++
|
||||
}
|
||||
|
||||
func TestToolHostScratchpadDeleteRemovesPadAndRefreshes(t *testing.T) {
|
||||
t.Setenv("XDG_DATA_HOME", t.TempDir())
|
||||
pads, err := scratchpad.Open("scratchpad-delete-host-test")
|
||||
if err != nil {
|
||||
t.Fatalf("scratchpad.Open: %v", err)
|
||||
}
|
||||
if _, err := pads.Write("doomed.md", "content", ""); err != nil {
|
||||
t.Fatalf("write doomed.md: %v", err)
|
||||
}
|
||||
recorder := &scratchpadChangeRecorder{}
|
||||
host := newToolHost(nil, pads, nil, preset.Set{}, nil, 120, 40)
|
||||
host.scratch = recorder
|
||||
|
||||
if err := host.ScratchpadDelete("doomed.md"); err != nil {
|
||||
t.Fatalf("ScratchpadDelete: %v", err)
|
||||
}
|
||||
if recorder.count != 1 {
|
||||
t.Fatalf("scratchpadsChanged calls = %d, want 1", recorder.count)
|
||||
}
|
||||
if _, _, err := pads.Read("doomed.md"); !errors.Is(err, os.ErrNotExist) {
|
||||
t.Fatalf("read deleted pad error = %v, want os.ErrNotExist", err)
|
||||
}
|
||||
if err := host.ScratchpadDelete("doomed.md"); !errors.Is(err, os.ErrNotExist) {
|
||||
t.Fatalf("delete missing error = %v, want os.ErrNotExist", err)
|
||||
}
|
||||
if recorder.count != 1 {
|
||||
t.Fatalf("scratchpadsChanged calls after failed delete = %d, want 1", recorder.count)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -358,6 +358,13 @@ func toolCatalog() []toolDescriptor {
|
||||
"content": stringProp("Text to append."),
|
||||
}, []string{"name", "content"}),
|
||||
},
|
||||
{
|
||||
Name: "scratchpad_delete",
|
||||
Description: "Delete a scratchpad entry.",
|
||||
InputSchema: objectSchema(map[string]any{
|
||||
"name": stringProp("Scratchpad name."),
|
||||
}, []string{"name"}),
|
||||
},
|
||||
{
|
||||
Name: "whoami",
|
||||
Description: "Return the caller's identity, role, parent, project metadata, and available tools.",
|
||||
|
||||
@@ -101,6 +101,7 @@ type ToolHost interface {
|
||||
ScratchpadRead(name string) (content string, revision string, err error)
|
||||
ScratchpadWrite(name, content, expectedRevision string) (revision string, err error)
|
||||
ScratchpadAppend(name, content string) error
|
||||
ScratchpadDelete(name string) error
|
||||
|
||||
// Meta.
|
||||
WhoAmI(callerID string) WhoAmI
|
||||
@@ -244,8 +245,8 @@ type TimerInfo struct {
|
||||
ID string `json:"timer_id"`
|
||||
Label string `json:"label,omitempty"`
|
||||
Body string `json:"body,omitempty"`
|
||||
Kind string `json:"kind"` // "delay" | "idle_any" | "idle_all"
|
||||
Status string `json:"status"` // "pending" | "paused"
|
||||
Kind string `json:"kind"` // "delay" | "idle_any" | "idle_all"
|
||||
Status string `json:"status"` // "pending" | "paused"
|
||||
OwnerID string `json:"owner_process_id"`
|
||||
WatchedIDs []string `json:"watched,omitempty"`
|
||||
FiresAtUnixMS int64 `json:"fires_at_unix_ms,omitempty"`
|
||||
@@ -776,6 +777,18 @@ func callTool(h ToolHost, callerID, method string, params json.RawMessage) (any,
|
||||
}
|
||||
return map[string]any{"ok": true}, 0, "", nil
|
||||
|
||||
case "scratchpad_delete":
|
||||
var p struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
if err := unmarshalParams(params, &p); err != nil {
|
||||
return nil, codeInvalidParams, err.Error(), nil
|
||||
}
|
||||
if err := h.ScratchpadDelete(p.Name); err != nil {
|
||||
return nil, codeInternal, err.Error(), nil
|
||||
}
|
||||
return map[string]any{"ok": true}, 0, "", nil
|
||||
|
||||
case "whoami":
|
||||
return h.WhoAmI(callerID), 0, "", nil
|
||||
|
||||
|
||||
Reference in New Issue
Block a user