@insitue/claude-plugin 0.0.3 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "insitue",
3
- "version": "0.0.3",
3
+ "version": "0.1.1",
4
4
  "description": "Drive a Claude Code session from the InSitue browser overlay. Pick an element in your app, claude reads the file and proposes the edit.",
5
5
  "mcpServers": {
6
6
  "insitue": {
@@ -19,14 +19,17 @@ Connect this Claude Code session to the running InSitue companion and turn each
19
19
 
20
20
  - **First**, call `mcp__insitue__list_recent_picks` once to see if the user already picked things before attaching. If there are recent picks the user hasn't acted on, summarise them so they can choose which to address.
21
21
  - **Then enter the loop**: call `mcp__insitue__next_pick`. It blocks for ~5 min by default. When it returns:
22
- - If `status: "ok"`: read the `pick.source.file` at `pick.source.line`, plus a few surrounding lines for context.
23
- - The `pick.userNote` is the user's instruction. If empty, ask "What would you like to change at `<componentName>` (`<file>:<line>`)?" before doing anything.
22
+ - If `status: "ok"`:
23
+ - **If `pick.userNote` is set**: the user typed an instruction in the browser panel before sending. Read the `pick.source.file` at `pick.source.line`, propose an edit, show the diff, wait for "approve"/"yes" before writing.
24
+ - **If `pick.userNote` is null** (user clicked without typing): the user might still be about to type in the browser panel — they have an ASK textbox in the panel that streams here as a `broadcast-ask` event. **Call `next_pick` AGAIN with a 30-second timeout** to wait for the follow-up note. The MCP bridge re-delivers the same pick with `userNote` populated once the user sends it. If that second call returns `status: "timeout"` instead, ONLY THEN fall back to asking "What would you like to change at `<componentName>` (`<file>:<line>`)?" in the terminal.
24
25
  - Propose an edit. Show a small diff in chat. Wait for the user to say "go" / "approve" / "yes" before writing.
25
26
  - On approval, apply with Edit/Write. Confirm what changed.
26
27
  - Loop back to `next_pick`.
27
28
  - **If a pick comes through with `target` starting with `[insitue]`**: the companion disconnected (HMR / restart). Tell the user, then call `next_pick` again — the bridge auto-reconnects.
28
29
  - **Exit the loop** when the user says "stop", "done", "quit", or similar.
29
30
 
31
+ **Why the double-poll on note-less picks**: the user's hands are on the browser, not the terminal. Their natural workflow is pick → type intent in the panel's ASK textbox → click Send. That ASK arrives via MCP as a `broadcast-ask` joined to the previous pick. If you ask in the terminal while the user is typing in the browser, both messages land at once and the user is confused. Default to "user is about to type in the browser" — re-poll first, only ask in the terminal as a last resort.
32
+
30
33
  ## Guardrails
31
34
 
32
35
  - Don't touch any file outside the pick's `pick.source.file` unless the user explicitly asks for cross-file changes.
@@ -61,19 +61,65 @@ var PickBuffer = class {
61
61
  if (this.picks.length > MAX_BUFFERED_PICKS) {
62
62
  this.picks.shift();
63
63
  }
64
+ this.flushWaiters();
65
+ }
66
+ /** #162: merge a `broadcast-ask` into the matching `pick.userNote`.
67
+ * Two ordering modes the user might create:
68
+ * 1. Pick first (auto on click), THEN type + Send → ask lands
69
+ * AFTER pick. If the pick is still queued (not yet delivered
70
+ * to claude), update it in place. If already delivered, the
71
+ * ask arrives as a standalone update — surfaced as a synthetic
72
+ * pick whose `userNote` is set so claude treats it as new
73
+ * input on the most-recent target.
74
+ * 2. Ask before pick is impossible (the panel needs a pick first
75
+ * to enable the ASK textbox), so we don't handle that. */
76
+ attachAsk(bundleId, text) {
77
+ const idx = this.picks.findIndex((p) => p.id === bundleId);
78
+ if (idx >= 0) {
79
+ this.picks[idx] = { ...this.picks[idx], userNote: text };
80
+ this.flushWaiters();
81
+ return;
82
+ }
83
+ if (this.lastDelivered === bundleId && this.lastDeliveredPick) {
84
+ this.picks.push({
85
+ ...this.lastDeliveredPick,
86
+ userNote: text,
87
+ at: (/* @__PURE__ */ new Date()).toISOString()
88
+ });
89
+ this.flushWaiters();
90
+ return;
91
+ }
92
+ const orphan = {
93
+ id: bundleId,
94
+ at: (/* @__PURE__ */ new Date()).toISOString(),
95
+ source: null,
96
+ confidence: "n/a",
97
+ target: "[insitue] note without pick",
98
+ selector: null,
99
+ userNote: text,
100
+ url: null,
101
+ componentStack: []
102
+ };
103
+ this.picks.push(orphan);
104
+ this.flushWaiters();
105
+ }
106
+ flushWaiters() {
64
107
  while (this.waiters.length && this.picks.length) {
65
108
  const w = this.waiters.shift();
66
109
  clearTimeout(w.timer);
67
110
  const next = this.picks.shift();
68
111
  this.lastDelivered = next.id;
112
+ this.lastDeliveredPick = next;
69
113
  w.resolve(next);
70
114
  }
71
115
  }
116
+ lastDeliveredPick = null;
72
117
  /** Resolve with the next pick to land OR null on timeout. */
73
118
  next(timeoutMs) {
74
119
  if (this.picks.length) {
75
120
  const next = this.picks.shift();
76
121
  this.lastDelivered = next.id;
122
+ this.lastDeliveredPick = next;
77
123
  return Promise.resolve(next);
78
124
  }
79
125
  return new Promise((resolve2) => {
@@ -120,7 +166,9 @@ function connectToCompanion(session) {
120
166
  t: "hello",
121
167
  // Pin to the published companion protocol version — bumped
122
168
  // when the wire format breaks, NOT for every release.
123
- protocolVersion: 4,
169
+ // v5 added broadcast-ask + subscribers-attached + agent-
170
+ // ask-external for the external-claude routing flow.
171
+ protocolVersion: 5,
124
172
  token: session.token
125
173
  })
126
174
  );
@@ -136,6 +184,13 @@ function connectToCompanion(session) {
136
184
  ws.send(JSON.stringify({ t: "subscribe" }));
137
185
  return;
138
186
  }
187
+ if (m && typeof m === "object" && m.t === "broadcast-ask") {
188
+ const ask = m;
189
+ if (typeof ask.bundleId === "string" && typeof ask.text === "string") {
190
+ buffer.attachAsk(ask.bundleId, ask.text);
191
+ }
192
+ return;
193
+ }
139
194
  if (m && typeof m === "object" && m.t === "broadcast-capture") {
140
195
  try {
141
196
  buffer.push(summariseBundle(m));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@insitue/claude-plugin",
3
- "version": "0.0.3",
3
+ "version": "0.1.1",
4
4
  "description": "Drive a Claude Code session from the InSitue browser overlay — pick an element in your app, claude reads the file and proposes the edit.",
5
5
  "license": "MIT",
6
6
  "type": "module",