mtech-ai-local-runner 0.1.114 → 0.1.115

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/README.md CHANGED
@@ -1,3 +1,15 @@
1
+ ## 0.1.115: close a request from the menu bar window
2
+
3
+ Open a request in the menu bar window and press **Close request…** to close it without answering. A sheet names the
4
+ request and takes an optional reason (up to 300 characters); nothing is sent until you press **Close request**. The
5
+ request leaves your list, its Slack card says who closed it, and the person who sent it sees it as closed. The Gateway
6
+ refuses the close while a local investigation for the request is still queued or waiting, and closing never stops or
7
+ changes an investigation.
8
+
9
+ A strict request, one that only the AI session it was sent to may accept or answer, no longer shows an answer box in
10
+ the window: it says "Answer this in the AI session it was sent to, or close it here." The menu bar app is now version
11
+ 1.4.0; `setup` installs it.
12
+
1
13
  ## 0.1.114: ask your AI from the menu bar window
2
14
 
3
15
  Select a request, decision, review or outcome record in the menu bar window, or a thread in its Rooms tab, and press
@@ -768,10 +768,11 @@ export class McpGatewayClient {
768
768
  const item = record(row) ?? {};
769
769
  if (typeof item.kind !== 'string' || typeof item.id !== 'string' || item.id === '')
770
770
  return [];
771
+ const boundSession = item.answer_needs_bound_session === true;
771
772
  return [{ kind: item.kind, id: item.id, workItemId: panelWorkItemId(item.work_item_id), title: text(item.title).slice(0, PANEL_TITLE),
772
773
  from: text(item.asked_by) || text(item.asked_by_actor_id), status: text(item.status),
773
774
  changedAt: Number.isSafeInteger(item.changed_at) ? item.changed_at : 0,
774
- answerable: item.kind === 'request' && item.answer_tool === 'work_request_answer' }];
775
+ answerable: item.kind === 'request' && item.answer_tool === 'work_request_answer' && !boundSession, boundSession }];
775
776
  }).slice(0, PANEL_ITEMS);
776
777
  }
777
778
  /** work_request_show for the request panel. */
@@ -793,6 +794,10 @@ export class McpGatewayClient {
793
794
  async answerWorkRequest(requestId, body, signal) {
794
795
  await this.callTool('work_request_answer', { request_id: requestId, body }, signal);
795
796
  }
797
+ /** work_request_close for the request panel, sent only after the person confirmed it in the app (Work Item af41c9bd). */
798
+ async closeWorkRequest(requestId, reason, signal) {
799
+ await this.callTool('work_request_close', reason === '' ? { request_id: requestId } : { request_id: requestId, reason }, signal);
800
+ }
796
801
  /** work_room_read activity, share_only (the projection the runner counts), for the menu bar Rooms tab: one page of 50. */
797
802
  async readRoomListPage(afterRoomId, signal) {
798
803
  const invalid = () => new GatewayToolError('Invalid Room list page', 'work_room_read', 'GATEWAY_INVALID_TOOL_RESULT');
@@ -1,4 +1,4 @@
1
- import { MAX_ANSWER_BYTES } from "./protocol.js";
1
+ import { MAX_ANSWER_BYTES, MAX_CLOSE_REASON_CHARS } from "./protocol.js";
2
2
  import { callRoom, checkRoomRequest } from "./room-panel.js";
3
3
  /** Each app message the panel answers is given up after this long. */
4
4
  export const PANEL_CALL_MS = 30_000;
@@ -8,7 +8,7 @@ const FAILED = "The runner could not reach the AI Gateway. Try again.";
8
8
  const AUDIENCE_CHANGED = "The Room members changed. Check the new list and post again.";
9
9
  /**
10
10
  * The daemon's side of the menu bar window (Gateway Work Items d2b6d0da and b35d65cb). It is the allowlist for what
11
- * the app may ask: the request panel lists, shows and answers Work Requests; the Rooms tab's messages are checked and
11
+ * the app may ask: the request panel lists, shows, answers and closes Work Requests; the Rooms tab's messages are checked and
12
12
  * called in room-panel.ts. Ids and text are checked before any call, one call runs at a time, and nothing is kept
13
13
  * after the reply. Logs carry the message type, an error code and counts only, never a name, title or body.
14
14
  */
@@ -25,7 +25,7 @@ export function createPanelHandler(input) {
25
25
  case "list":
26
26
  return { items: (await input.gateway.readInboxForPanel(signal)).map(item => ({ kind: item.kind, id: item.id,
27
27
  work_item_id: item.workItemId, title: item.title, from: item.from, status: item.status, changed_at: item.changedAt,
28
- answerable: item.answerable })) };
28
+ answerable: item.answerable, closable: item.kind === "request", bound_session: item.boundSession })) };
29
29
  case "show": {
30
30
  const shown = await input.gateway.showWorkRequest(request.requestId, signal);
31
31
  return { request: { id: shown.id, work_item_id: shown.workItemId, title: shown.title, body: shown.body,
@@ -35,6 +35,9 @@ export function createPanelHandler(input) {
35
35
  case "answer":
36
36
  await input.gateway.answerWorkRequest(request.requestId, request.body.trim(), signal);
37
37
  return { answered: true };
38
+ case "close":
39
+ await input.gateway.closeWorkRequest(request.requestId, request.reason.trim(), signal);
40
+ return { closed: true };
38
41
  default:
39
42
  return callRoom(request, input.gateway, signal);
40
43
  }
@@ -55,6 +58,13 @@ export function createPanelHandler(input) {
55
58
  }
56
59
  catch { /* A refresh that fails never undoes a sent answer. */ }
57
60
  }
61
+ if (request.type === "close") {
62
+ log({ event: "menubar_panel_closed" });
63
+ try {
64
+ input.onAnswered();
65
+ }
66
+ catch { /* A refresh that fails never undoes a close. */ }
67
+ }
58
68
  if (request.type === "room_presented")
59
69
  log({ event: "menubar_panel_presented", count: data.presented });
60
70
  if (request.type === "room_mark_read") {
@@ -87,10 +97,13 @@ export function createPanelHandler(input) {
87
97
  function check(request) {
88
98
  if (request.type === "list")
89
99
  return null;
90
- if (request.type !== "show" && request.type !== "answer")
100
+ if (request.type !== "show" && request.type !== "answer" && request.type !== "close")
91
101
  return checkRoomRequest(request);
92
102
  if (!ID.test(request.requestId))
93
103
  return "That request id is not valid.";
104
+ if (request.type === "close" && [...request.reason.trim()].length > MAX_CLOSE_REASON_CHARS) {
105
+ return `A reason can be at most ${MAX_CLOSE_REASON_CHARS} characters.`;
106
+ }
94
107
  if (request.type === "answer") {
95
108
  const bytes = Buffer.byteLength(request.body.trim());
96
109
  if (bytes === 0)
@@ -17,6 +17,8 @@
17
17
  * {"v":1,"type":"list","req":"<uuid>"}
18
18
  * {"v":1,"type":"show","req":"<uuid>","request_id":"<uuid>"}
19
19
  * {"v":1,"type":"answer","req":"<uuid>","request_id":"<uuid>","body":"…"}
20
+ * {"v":1,"type":"close","req":"<uuid>","request_id":"<uuid>","reason":"…"} sent only after the person confirmed;
21
+ * reason may be empty (Work Item af41c9bd)
20
22
  * {"v":1,"type":"reply","req":"<uuid>","ok":true,"data":{…}}
21
23
  * {"v":1,"type":"reply","req":"<uuid>","ok":false,"error":"busy|invalid|refused|failed","message":"…"}
22
24
  *
@@ -42,6 +44,8 @@ export const MENUBAR_SOCKET = "menubar.sock";
42
44
  export const MAX_MENUBAR_LINE_BYTES = 4 * 1024 * 1024;
43
45
  /** The Gateway's own limit for one answer, in UTF-8 bytes after trimming. */
44
46
  export const MAX_ANSWER_BYTES = 8_000;
47
+ /** The Gateway's own limit for a close reason, in characters after trimming (Work Item af41c9bd). */
48
+ export const MAX_CLOSE_REASON_CHARS = 300;
45
49
  /** The Gateway's own limit for one Room message, in UTF-8 bytes after trimming. */
46
50
  export const MAX_ROOM_POST_BYTES = 8_000;
47
51
  const NOTICE_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
@@ -49,7 +53,7 @@ const APP_VERSION = /^\d+\.\d+\.\d+$/;
49
53
  /** No legitimate message carries more ids than this; the handler checks the real limits. */
50
54
  const MAX_LIST_ITEMS = 1_000;
51
55
  export const ASK_AI_SUBJECTS = ["work_request", "work_decision", "work_item_review", "outcome_record"];
52
- const PANEL_TYPES = new Set(["list", "show", "answer", "room_list", "room_show", "room_thread", "room_presented",
56
+ const PANEL_TYPES = new Set(["list", "show", "answer", "close", "room_list", "room_show", "room_thread", "room_presented",
53
57
  "room_mark_read", "room_post"]);
54
58
  /** True for every message the panel handler answers with a reply. */
55
59
  export function isPanelRequest(message) {
@@ -100,6 +104,9 @@ export function parseAppLine(line) {
100
104
  case "answer":
101
105
  return keys === 5 && isId(record.req) && typeof record.request_id === "string" && typeof record.body === "string"
102
106
  ? { type: "answer", req: record.req, requestId: record.request_id, body: record.body } : null;
107
+ case "close":
108
+ return keys === 5 && isId(record.req) && typeof record.request_id === "string" && typeof record.reason === "string"
109
+ ? { type: "close", req: record.req, requestId: record.request_id, reason: record.reason } : null;
103
110
  case "room_list":
104
111
  return keys === 3 && isId(record.req) ? { type: "room_list", req: record.req } : null;
105
112
  case "room_show":
@@ -7,8 +7,8 @@
7
7
  <key>CFBundleDisplayName</key><string>AI Gateway</string>
8
8
  <key>CFBundleExecutable</key><string>AIGatewayMenubar</string>
9
9
  <key>CFBundlePackageType</key><string>APPL</string>
10
- <key>CFBundleShortVersionString</key><string>1.3.0</string>
11
- <key>CFBundleVersion</key><string>4</string>
10
+ <key>CFBundleShortVersionString</key><string>1.4.0</string>
11
+ <key>CFBundleVersion</key><string>5</string>
12
12
  <key>LSMinimumSystemVersion</key><string>13.0</string>
13
13
  <key>LSUIElement</key><true/>
14
14
  </dict>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mtech-ai-local-runner",
3
- "version": "0.1.114",
3
+ "version": "0.1.115",
4
4
  "private": false,
5
5
  "license": "UNLICENSED",
6
6
  "bin": {