mtech-ai-local-runner 0.1.84 → 0.1.85

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
@@ -291,18 +291,23 @@ reports `CODEX_HOOKS_JSON_INVALID`. `uninstall` removes both integrations, the s
291
291
 
292
292
  The Claude integration also installs the Work Room pane, the mod `ai-gateway-work-room@mtec-ai-gateway` (Work Item
293
293
  98da2635). A band above the prompt says how many Room shares and requests are waiting. Its button, or `/work-room`,
294
- opens a pane beside the conversation. The pane lists what waits for you and the Rooms you are in, and it opens Room
295
- threads. From the pane you can answer a request, reply in a thread, share an answer to a Room or mark messages read.
296
- Opening a Room shows its threads with unread messages first and every unread message in them, and Mark all read marks
297
- up to 100 of them at once. Clear hides a request, decision or review on this Mac only, with no Gateway call; Show
298
- under "Cleared" lists them again with Restore.
294
+ opens a pane beside the conversation. "Requests for me" shows each request, decision and review in its own box, with
295
+ who asked first. "Rooms" shows each Room in its own box, with its threads and messages indented under it. From the
296
+ pane you can answer a request, reply in a thread, share an answer to a Room or mark messages read. Each of these
297
+ starts with Send (or the button that names it), then a confirm row that names who will see it, then Claude's
298
+ permission dialog. Opening a Room shows its threads with unread messages first and every unread message in them, and
299
+ Mark all read marks up to 100 of them at once.
300
+
301
+ Ask Claude puts only ids and tool names in the conversation and asks Claude to read the full request and sum it up
302
+ in one line; nothing is sent, and it is the only button that starts a model turn. Hide hides a request, decision or
303
+ review on this Mac only, with no Gateway call: it stays open for the person who asked and on your other devices.
304
+ "Hidden (N)" with Show lists them again, each with Unhide. The pane calls the AI Gateway only through your own AI
305
+ Gateway connector in Claude; without the connector the pane says so. The Claude mobile app draws no input field, so
306
+ there the pane reads, shares, marks read and offers Ask Claude.
307
+
299
308
  After a Gateway change to a tool's arguments, refresh the AI Gateway tools list in Claude (Settings, Connectors, AI
300
309
  Gateway) and start a new session: a session drops arguments its tools list does not declare. The pane says so when
301
310
  its Room reads lose their arguments.
302
- Each of these is sent only after a confirm row that names who will see it. The pane calls the AI Gateway only through
303
- your own AI Gateway connector in Claude, and it never starts a model turn: its Open in chat button puts only ids in the
304
- conversation. Without the connector the pane says so. The Claude mobile app draws no input field, so there the pane
305
- reads, shares and marks read only.
306
311
 
307
312
  The pane calls the connector with no model turn, so a Claude session in auto mode would refuse every one of those
308
313
  calls. The mod therefore ships PreToolUse hooks that match any AI Gateway connector and apply to every caller in the
@@ -1 +1 @@
1
- { "name": "ai-gateway-work-room", "version": "1.3.0", "description": "Work Room shares and requests from colleagues beside the conversation, installed by mtech-ai-local-runner (Work Item 98da2635).", "types": "./types/index.d.ts" }
1
+ { "name": "ai-gateway-work-room", "version": "1.4.0", "description": "Work Room shares and requests from colleagues beside the conversation, installed by mtech-ai-local-runner (Work Item 98da2635).", "types": "./types/index.d.ts" }
@@ -102,12 +102,12 @@ export async function refreshCounts($: EngineInterface): Promise<void> {
102
102
  await update($, status, () => (current === false ? { state: 'stale', detail: STALE } : { state: 'ok', detail: '' }))
103
103
  }
104
104
 
105
- /** Clear: hides the item on this Mac only. No Gateway call; the item stays open there and on other Macs. */
105
+ /** Hide (stored as cleared): on this Mac only. No Gateway call; the item stays open there and on other Macs. */
106
106
  export async function clearItem($: EngineInterface, item: InboxItem): Promise<void> {
107
107
  try {
108
108
  await $.store.set(`${CLEARED}${item.id}`, item.kind)
109
109
  } catch {
110
- return fail($, { state: 'error', detail: 'Clear could not be saved on this Mac.' })
110
+ return fail($, { state: 'error', detail: 'Hide could not be saved on this Mac.' })
111
111
  }
112
112
  await update($, cleared, ids => (ids.includes(item.id) ? ids : [...ids, item.id]))
113
113
  await recount($)
@@ -117,7 +117,7 @@ export async function restoreItem($: EngineInterface, id: string): Promise<void>
117
117
  try {
118
118
  await $.store.delete(`${CLEARED}${id}`)
119
119
  } catch {
120
- return fail($, { state: 'error', detail: 'Restore could not be saved on this Mac.' })
120
+ return fail($, { state: 'error', detail: 'Unhide could not be saved on this Mac.' })
121
121
  }
122
122
  await update($, cleared, ids => ids.filter(other => other !== id))
123
123
  await recount($)
@@ -180,7 +180,9 @@ export async function reportPresented($: EngineInterface, roomId: string, ids: s
180
180
  /** Writes already confirmed in this run, by write key, so a second press of the same text sends once. */
181
181
  const sent = new Set<string>()
182
182
 
183
- const CONFIRM_LABEL: Record<NonNullable<Pending>['kind'], string> = { answer: 'Send', post: 'Post', answer_share: 'Share', acknowledge: 'Mark read' }
183
+ const KIND_LABEL: Record<InboxItem['kind'], string> = { request: 'Request', decision: 'Decision', review: 'Review request' }
184
+ const ASK_CLAUDE_HINT = 'Ask Claude: Claude reads the full request here and sums it up. Nothing is sent.'
185
+ const HIDE_HINT = 'Hide: only on this Mac; it stays open for the person who asked.'
184
186
 
185
187
  async function stage($: EngineInterface, next: Pending): Promise<void> {
186
188
  await update($, pending, () => next)
@@ -256,7 +258,7 @@ export const register: Register = on => {
256
258
  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
257
259
  const table = $.ui.resolve(e)
258
260
  const { Box, Button, Text } = table
259
- // The mobile app draws no field yet: there the pane reads, shares and marks read, and answers go through Open in chat.
261
+ // The mobile app draws no field yet: there the pane reads, shares and marks read, and answers go through Ask Claude.
260
262
  const Input = 'Input' in table ? table.Input : undefined
261
263
  const state = await read($, status)
262
264
  const job = await read($, pending)
@@ -275,105 +277,120 @@ export const register: Register = on => {
275
277
  {job && (
276
278
  <Box>
277
279
  <Text color="yellow">{job.summary} </Text>
278
- <Button key="confirm" label={CONFIRM_LABEL[job.kind]} onPress={() => quietly(confirm($))} />
280
+ <Button key="confirm" label="Confirm" onPress={() => quietly(confirm($))} />
279
281
  <Button key="cancel" label="Cancel" onPress={() => quietly(cancel($))} />
280
282
  </Box>
281
283
  )}
282
- <Text bold>Requests for me</Text>
283
- {visible.length === 0 && <Text dimColor>None.</Text>}
284
- {visible.map(item => (
285
- <Box flexDirection="column">
286
- <Text>[{item.kind}] {item.title} — {item.askedBy}</Text>
287
- {item.kind === 'request' && Input && (
288
- <Input
289
- key={`answer-${item.id}`}
290
- placeholder="Answer this request"
291
- rows={3}
292
- submitLabel="review"
293
- onSubmit={(text: string) => quietly(stage($, { kind: 'answer', target: item.id, text, summary: `Answer goes to ${item.askedBy} only.` }))}
294
- />
295
- )}
296
- <Button key={`chat-${item.id}`} label="Open in chat" onPress={() => quietly($.prompt.submit({ text: chatPrompt(item) }))} />
297
- <Button key={`clear-${item.id}`} label="Clear" onPress={() => quietly(clearItem($, item))} />
298
- </Box>
299
- ))}
300
- {clearedItems.length > 0 && (
301
- <Box>
302
- <Text dimColor>Cleared ({clearedItems.length}) </Text>
303
- <Button key="show-cleared" label={showHidden ? 'Hide' : 'Show'} onPress={() => quietly(update($, showCleared, shown => !shown))} />
304
- </Box>
305
- )}
306
- {showHidden && clearedItems.map(item => (
307
- <Box>
308
- <Text dimColor>[{item.kind}] {item.title} — {item.askedBy} </Text>
309
- <Button key={`restore-${item.id}`} label="Restore" onPress={() => quietly(restoreItem($, item.id))} />
310
- </Box>
311
- ))}
312
- <Text bold>Room shares</Text>
313
- {rows.length === 0 && <Text dimColor>No Rooms.</Text>}
314
- {rows.map(row => {
315
- const room = open[row.roomId]
316
- if (room) toReport.push([row.roomId, room.threads.flatMap(thread => thread.messages.filter(m => !m.presented).map(m => m.id))])
317
-
318
- return (
319
- <Box flexDirection="column">
320
- <Box>
321
- <Text>{row.name} ({row.unread} unread) </Text>
322
- <Button
323
- key={`open-${row.roomId}`}
324
- label={room ? 'Refresh' : 'Open'}
325
- onPress={() => quietly(openRoom($, row.roomId, `The user pressed Open on Room ${row.name} in the Work Room pane.`))}
326
- />
327
- </Box>
328
- {room && markable(room.threads).length > 0 && (
329
- <Button
330
- key={`ack-room-${row.roomId}`}
331
- label="Mark all read"
332
- onPress={() => {
333
- const ids = markable(room.threads)
334
- quietly(stage($, { kind: 'acknowledge', target: `${row.roomId}|`, text: ids.join(','), summary: `Mark ${ids.length} message${ids.length === 1 ? '' : 's'} read?` }))
335
- }}
284
+ <Box flexDirection="column" gap="xs">
285
+ <Text bold>Requests for me ({visible.length})</Text>
286
+ {visible.length === 0 && <Text dimColor>None.</Text>}
287
+ {visible.map(item => (
288
+ <Box flexDirection="column" gap="xs" borderStyle="round" borderDimColor paddingX={1}>
289
+ <Text dimColor>{KIND_LABEL[item.kind]} from {item.askedBy}</Text>
290
+ <Text>{item.title}</Text>
291
+ {item.kind === 'request' && Input && (
292
+ <Input
293
+ key={`answer-${item.id}`}
294
+ placeholder="Answer this request"
295
+ rows={3}
296
+ submitLabel="Send"
297
+ onSubmit={(text: string) => quietly(stage($, { kind: 'answer', target: item.id, text, summary: `Answer goes to ${item.askedBy} only.` }))}
336
298
  />
337
299
  )}
338
- {room?.threads.map(thread => (
339
- <Box flexDirection="column">
340
- <Text dimColor># {thread.title}</Text>
341
- {thread.messages.map(m => (
342
- <Box flexDirection="column">
343
- <Text>{m.author}: {m.body}</Text>
344
- {m.requestId !== undefined && (
300
+ <Box flexDirection="row" gap="xs">
301
+ <Button key={`chat-${item.id}`} label="Ask Claude" onPress={() => quietly($.prompt.submit({ text: chatPrompt(item) }))} />
302
+ <Button key={`hide-${item.id}`} label="Hide" onPress={() => quietly(clearItem($, item))} />
303
+ </Box>
304
+ </Box>
305
+ ))}
306
+ {visible.length > 0 && <Text dimColor>{ASK_CLAUDE_HINT}</Text>}
307
+ {visible.length > 0 && <Text dimColor>{HIDE_HINT}</Text>}
308
+ {clearedItems.length > 0 && (
309
+ <Box flexDirection="row" gap="xs" alignItems="center">
310
+ <Text dimColor>Hidden ({clearedItems.length})</Text>
311
+ <Button key="show-hidden" label={showHidden ? 'Close' : 'Show'} onPress={() => quietly(update($, showCleared, shown => !shown))} />
312
+ </Box>
313
+ )}
314
+ {showHidden && clearedItems.map(item => (
315
+ <Box flexDirection="row" gap="xs" alignItems="center">
316
+ <Text dimColor>{KIND_LABEL[item.kind]} from {item.askedBy}: {item.title}</Text>
317
+ <Button key={`unhide-${item.id}`} label="Unhide" onPress={() => quietly(restoreItem($, item.id))} />
318
+ </Box>
319
+ ))}
320
+ </Box>
321
+ <Box flexDirection="column" gap="xs" marginTop={1}>
322
+ <Text bold>Rooms</Text>
323
+ {rows.length === 0 && <Text dimColor>No Rooms.</Text>}
324
+ {rows.map(row => {
325
+ const room = open[row.roomId]
326
+ if (room) toReport.push([row.roomId, room.threads.flatMap(thread => thread.messages.filter(m => !m.presented).map(m => m.id))])
327
+
328
+ return (
329
+ <Box flexDirection="column" gap="xs" borderStyle="round" borderDimColor paddingX={1}>
330
+ <Box flexDirection="row" justifyContent="space-between" alignItems="center" gap="xs">
331
+ <Text>{row.name} · {row.unread} unread</Text>
332
+ <Box flexDirection="row" gap="xs">
333
+ <Button
334
+ key={`open-${row.roomId}`}
335
+ label={room ? 'Refresh' : 'Open'}
336
+ onPress={() => quietly(openRoom($, row.roomId, `The user pressed Open on Room ${row.name} in the Work Room pane.`))}
337
+ />
338
+ {room && markable(room.threads).length > 0 && (
339
+ <Button
340
+ key={`ack-room-${row.roomId}`}
341
+ label="Mark all read"
342
+ onPress={() => {
343
+ const ids = markable(room.threads)
344
+ quietly(stage($, { kind: 'acknowledge', target: `${row.roomId}|`, text: ids.join(','), summary: `Mark ${ids.length} message${ids.length === 1 ? '' : 's'} read?` }))
345
+ }}
346
+ />
347
+ )}
348
+ </Box>
349
+ </Box>
350
+ {room?.threads.map(thread => (
351
+ <Box flexDirection="column" gap="xs" paddingLeft={2}>
352
+ <Box flexDirection="row" justifyContent="space-between" alignItems="center" gap="xs">
353
+ <Text bold dimColor># {thread.title}</Text>
354
+ {markable([thread]).length > 0 && (
345
355
  <Button
346
- key={`share-${m.id}`}
347
- label="Share answer to Room"
348
- onPress={() => quietly(stage($, { kind: 'answer_share', target: `${row.roomId}|${thread.id}|${room.info.membershipRevision}|${m.requestId}`, text: m.requestId ?? '', summary: `Shares your answer to ${room.info.name} for ${room.info.memberCount} members.` }))}
356
+ key={`ack-${thread.id}`}
357
+ label="Mark read"
358
+ onPress={() => {
359
+ const ids = markable([thread])
360
+ quietly(stage($, { kind: 'acknowledge', target: `${row.roomId}|${thread.id}`, text: ids.join(','), summary: `Mark ${ids.length} message${ids.length === 1 ? '' : 's'} read?` }))
361
+ }}
349
362
  />
350
363
  )}
351
364
  </Box>
352
- ))}
353
- {markable([thread]).length > 0 && (
354
- <Button
355
- key={`ack-${thread.id}`}
356
- label="Mark read"
357
- onPress={() => {
358
- const ids = markable([thread])
359
- quietly(stage($, { kind: 'acknowledge', target: `${row.roomId}|${thread.id}`, text: ids.join(','), summary: `Mark ${ids.length} message${ids.length === 1 ? '' : 's'} read?` }))
360
- }}
361
- />
362
- )}
363
- {Input && (
364
- <Input
365
- key={`post-${thread.id}`}
366
- placeholder="Reply in this thread"
367
- rows={2}
368
- submitLabel="review"
369
- onSubmit={(text: string) => quietly(stage($, { kind: 'post', target: `${row.roomId}|${thread.id}|${room.info.membershipRevision}`, text, summary: `Posts to ${room.info.name} for ${room.info.memberCount} members.` }))}
370
- />
371
- )}
372
- </Box>
373
- ))}
374
- </Box>
375
- )
376
- })}
365
+ <Box flexDirection="column" gap="xs" paddingLeft={2}>
366
+ {thread.messages.map(m => (
367
+ <Box flexDirection="column">
368
+ <Text>{m.author}: {m.body}</Text>
369
+ {m.requestId !== undefined && (
370
+ <Button
371
+ key={`share-${m.id}`}
372
+ label="Share answer to Room"
373
+ onPress={() => quietly(stage($, { kind: 'answer_share', target: `${row.roomId}|${thread.id}|${room.info.membershipRevision}|${m.requestId}`, text: m.requestId ?? '', summary: `Shares your answer to ${room.info.name} for ${room.info.memberCount} members.` }))}
374
+ />
375
+ )}
376
+ </Box>
377
+ ))}
378
+ {Input && (
379
+ <Input
380
+ key={`post-${thread.id}`}
381
+ placeholder="Reply in this thread"
382
+ rows={2}
383
+ submitLabel="Send"
384
+ onSubmit={(text: string) => quietly(stage($, { kind: 'post', target: `${row.roomId}|${thread.id}|${room.info.membershipRevision}`, text, summary: `Posts to ${room.info.name} for ${room.info.memberCount} members.` }))}
385
+ />
386
+ )}
387
+ </Box>
388
+ </Box>
389
+ ))}
390
+ </Box>
391
+ )
392
+ })}
393
+ </Box>
377
394
  </Box>
378
395
  )
379
396
  for (const [roomId, ids] of toReport) quietly(reportPresented($, roomId, ids))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mtech-ai-local-runner",
3
- "version": "0.1.84",
3
+ "version": "0.1.85",
4
4
  "private": false,
5
5
  "license": "UNLICENSED",
6
6
  "bin": {