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 +14 -9
- package/claude-mod/.claude-plugin/plugin.json +1 -1
- package/claude-mod/hooks/register.tsx +112 -95
- package/package.json +1 -1
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.
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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.
|
|
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
|
-
/**
|
|
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: '
|
|
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: '
|
|
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
|
|
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
|
|
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=
|
|
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
|
-
<
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
<
|
|
287
|
-
|
|
288
|
-
<
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
|
|
339
|
-
<
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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={`
|
|
347
|
-
label="
|
|
348
|
-
onPress={() =>
|
|
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
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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))
|