mtech-ai-local-runner 0.1.111 → 0.1.113
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 +32 -49
- package/dist/src/adapters/claude-code/adapter.js +16 -7
- package/dist/src/adapters/claude-code/stream-parser.js +65 -4
- package/dist/src/adapters/runtime-failure.js +34 -0
- package/dist/src/asks/claude-mod.js +48 -87
- package/dist/src/asks/commands.js +17 -33
- package/dist/src/asks/doctor.js +3 -4
- package/dist/src/cli.js +3 -3
- package/dist/src/daemon/codex-app-host-entry.js +94 -0
- package/dist/src/daemon/codex-app-setup.js +125 -0
- package/dist/src/daemon/codex-app-tools-host.js +82 -0
- package/dist/src/daemon/execution-coordinator.js +7 -4
- package/dist/src/daemon/runtime.js +74 -11
- package/dist/src/safety/claude-production-host.js +14 -3
- package/dist/src/safety/claude-review-provider.js +9 -1
- package/dist/src/safety/d4-execution-limits.js +23 -11
- package/dist/src/safety/d4-host-composition.js +139 -0
- package/dist/src/safety/d4-reviewed-hosts.js +2 -0
- package/dist/src/safety/d4-runtime-release.js +9 -0
- package/dist/src/verification/p1-execution-host.js +4 -0
- package/package.json +1 -3
- package/claude-mod/.claude-plugin/marketplace.json +0 -1
- package/claude-mod/.claude-plugin/plugin.json +0 -1
- package/claude-mod/hooks/gateway.ts +0 -47
- package/claude-mod/hooks/hooks.json +0 -34
- package/claude-mod/hooks/model.ts +0 -268
- package/claude-mod/hooks/register.tsx +0 -460
- package/claude-mod/tsconfig.json +0 -3
- package/claude-mod/types/index.d.ts +0 -28
|
@@ -1,460 +0,0 @@
|
|
|
1
|
-
import { atom, read, update } from 'claude-code'
|
|
2
|
-
import type { EngineInterface, Register } from 'claude-code'
|
|
3
|
-
|
|
4
|
-
import type { Counts, InboxItem, OpenRoom, Pending, RoomActivity, RoomMessage, Status, ThreadRow } from '../types'
|
|
5
|
-
import { callGateway } from './gateway'
|
|
6
|
-
import { CLEARED, INBOX_LIMIT, bandText, authorName, chatPrompt, clearedIds, detailLine, findPayload, groupInbox, markable, mergeMessages, parseActivity, parseInbox, parseMessages, parseRoom, parseThreads, pickThreads, visibleInbox, writeKey } from './model'
|
|
7
|
-
|
|
8
|
-
/*
|
|
9
|
-
* The Work Room pane (Work Item 98da2635, spec 2026-10-30): Room shares and requests from colleagues beside the
|
|
10
|
-
* conversation, read and answered through the person's own AI Gateway connector with no model turn. The band above
|
|
11
|
-
* the prompt says what waits; the pane opens only from the band or /work-room, never at session start.
|
|
12
|
-
*
|
|
13
|
-
* Auto mode refuses every call the pane makes, since none comes from a request of the person's in the conversation, and
|
|
14
|
-
* a mod's own module hooks cannot approve its own calls. So hooks/hooks.json carries PreToolUse hooks for every caller
|
|
15
|
-
* (owner decisions 2026-10-31): they allow the reads (work_room_read, work_inbox) and the presented receipt, and they
|
|
16
|
-
* put each write (answer, post, share, mark read) to the person in Claude's own permission dialog, after the pane's
|
|
17
|
-
* confirm press.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
export const PANE = 'work-room'
|
|
21
|
-
const TITLE = 'Work Room'
|
|
22
|
-
const REFRESH_MS = 60_000
|
|
23
|
-
const NO_CONNECTOR = 'This session has no AI Gateway Work Room tools. Connect AI Gateway in Claude, reconnect it with /mcp, or start a new session.'
|
|
24
|
-
// A session keeps the tools list its connector had when it started, and a call drops arguments that list does not
|
|
25
|
-
// declare. With a list from before unread_only and latest (Gateway a8df571a), a Room opens with its oldest messages.
|
|
26
|
-
const STALE = 'Refresh the AI Gateway tools list in Claude (Settings, Connectors, AI Gateway), then start a new session, so Rooms show their newest and unread messages.'
|
|
27
|
-
const NIL = '00000000-0000-4000-8000-000000000000'
|
|
28
|
-
|
|
29
|
-
const counts = atom({ plugin: 'ai-gateway-work-room', key: 'counts' } as const, { shares: 0, requests: 0 } as Counts)
|
|
30
|
-
const activity = atom({ plugin: 'ai-gateway-work-room', key: 'activity' } as const, [] as RoomActivity[])
|
|
31
|
-
const inbox = atom({ plugin: 'ai-gateway-work-room', key: 'inbox' } as const, [] as InboxItem[])
|
|
32
|
-
const rooms = atom({ plugin: 'ai-gateway-work-room', key: 'rooms' } as const, {} as Record<string, OpenRoom>)
|
|
33
|
-
const pending = atom({ plugin: 'ai-gateway-work-room', key: 'pending' } as const, null as Pending)
|
|
34
|
-
const status = atom({ plugin: 'ai-gateway-work-room', key: 'status' } as const, { state: 'ok', detail: '' } as Status)
|
|
35
|
-
const cleared = atom({ plugin: 'ai-gateway-work-room', key: 'cleared' } as const, [] as string[])
|
|
36
|
-
const showCleared = atom({ plugin: 'ai-gateway-work-room', key: 'showCleared' } as const, false as boolean)
|
|
37
|
-
const tab = atom({ plugin: 'ai-gateway-work-room', key: 'tab' } as const, 'requests' as 'requests' | 'rooms')
|
|
38
|
-
// The request group the person opened; null opens the newest one.
|
|
39
|
-
const openGroup = atom({ plugin: 'ai-gateway-work-room', key: 'openGroup' } as const, null as string | null)
|
|
40
|
-
const nowMs = atom({ plugin: 'ai-gateway-work-room', key: 'nowMs' } as const, 0 as number)
|
|
41
|
-
|
|
42
|
-
/** Whether this session's calls carry unread_only and latest to the Gateway; asked once per run. */
|
|
43
|
-
let readOptions: boolean | undefined
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* The Gateway refuses unread_only with latest before it looks up the Room. A call that loses both arguments on the
|
|
47
|
-
* way gets "Work Room is unavailable." instead. Undefined when the answer says neither (a refusal, no connector).
|
|
48
|
-
*/
|
|
49
|
-
async function readOptionsReach($: EngineInterface): Promise<boolean | undefined> {
|
|
50
|
-
if (readOptions !== undefined) return readOptions
|
|
51
|
-
const probe = await callGateway($, 'work_room_read', { operation: 'messages', room_id: NIL, thread_id: NIL, unread_only: true, latest: true, limit: 1 })
|
|
52
|
-
if (probe.ok || probe.state !== 'error') return undefined
|
|
53
|
-
readOptions = probe.detail.includes('not both')
|
|
54
|
-
|
|
55
|
-
return readOptions
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** Messages already sent for presented in this run, so a redraw before the update lands does not report them twice. */
|
|
59
|
-
const presentedSent = new Set<string>()
|
|
60
|
-
|
|
61
|
-
/** Background work the person did not wait for: a failure is already shown through status, never thrown. */
|
|
62
|
-
function quietly(work: Promise<unknown>): void {
|
|
63
|
-
work.catch(() => undefined)
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
async function fail($: EngineInterface, answer: { state: Status['state']; detail: string }): Promise<void> {
|
|
67
|
-
await update($, status, () => ({ state: answer.state, detail: answer.state === 'no-connector' ? NO_CONNECTOR : answer.detail }))
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** The band's counts from what this session holds: Room unread, and requests not cleared on this Mac. */
|
|
71
|
-
async function recount($: EngineInterface): Promise<void> {
|
|
72
|
-
const rows = await read($, activity)
|
|
73
|
-
const items = visibleInbox(await read($, inbox), await read($, cleared))
|
|
74
|
-
await update($, counts, () => ({ shares: rows.reduce((sum, row) => sum + row.unread, 0), requests: items.length }))
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** Reads the cleared ids from the store. A complete inbox read drops ids it no longer lists (answered, decided or expired). */
|
|
78
|
-
async function syncCleared($: EngineInterface, items: InboxItem[] | undefined): Promise<void> {
|
|
79
|
-
try {
|
|
80
|
-
let ids = clearedIds(await $.store.keys())
|
|
81
|
-
if (items !== undefined && items.length < INBOX_LIMIT) {
|
|
82
|
-
const open = new Set(items.map(item => item.id))
|
|
83
|
-
for (const id of ids.filter(id => !open.has(id))) await $.store.delete(`${CLEARED}${id}`)
|
|
84
|
-
ids = ids.filter(id => open.has(id))
|
|
85
|
-
}
|
|
86
|
-
await update($, cleared, () => ids)
|
|
87
|
-
} catch {
|
|
88
|
-
// An unreadable store keeps the ids this session already holds; the counts still show.
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/** Counts only: Room activity and open inbox items, never message bodies. Each half shows even when the other fails. */
|
|
93
|
-
export async function refreshCounts($: EngineInterface): Promise<void> {
|
|
94
|
-
// Ages are a nicety: a host without a clock shows rows without them.
|
|
95
|
-
const now = await $.clock.now().catch(() => 0)
|
|
96
|
-
await update($, nowMs, () => now)
|
|
97
|
-
const [roomAnswer, inboxAnswer] = await Promise.all([
|
|
98
|
-
callGateway($, 'work_room_read', { operation: 'activity', limit: 100 }),
|
|
99
|
-
callGateway($, 'work_inbox', { direction: 'to_me' }),
|
|
100
|
-
])
|
|
101
|
-
if (roomAnswer.ok) await update($, activity, () => parseActivity(findPayload(roomAnswer.payload, v => Array.isArray(v.rooms))) ?? [])
|
|
102
|
-
const listed = inboxAnswer.ok ? parseInbox(findPayload(inboxAnswer.payload, v => Array.isArray(v.items))) : undefined
|
|
103
|
-
if (inboxAnswer.ok) await update($, inbox, () => listed ?? [])
|
|
104
|
-
await syncCleared($, listed)
|
|
105
|
-
await recount($)
|
|
106
|
-
if (!roomAnswer.ok) return fail($, roomAnswer)
|
|
107
|
-
if (!inboxAnswer.ok) return fail($, inboxAnswer)
|
|
108
|
-
const current = await readOptionsReach($)
|
|
109
|
-
await update($, status, () => (current === false ? { state: 'stale', detail: STALE } : { state: 'ok', detail: '' }))
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/** Hide (stored as cleared): on this Mac only. No Gateway call; the item stays open there and on other Macs. */
|
|
113
|
-
export async function clearItem($: EngineInterface, item: InboxItem): Promise<void> {
|
|
114
|
-
try {
|
|
115
|
-
await $.store.set(`${CLEARED}${item.id}`, item.kind)
|
|
116
|
-
} catch {
|
|
117
|
-
return fail($, { state: 'error', detail: 'Hide could not be saved on this Mac.' })
|
|
118
|
-
}
|
|
119
|
-
await update($, cleared, ids => (ids.includes(item.id) ? ids : [...ids, item.id]))
|
|
120
|
-
await recount($)
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
export async function restoreItem($: EngineInterface, id: string): Promise<void> {
|
|
124
|
-
try {
|
|
125
|
-
await $.store.delete(`${CLEARED}${id}`)
|
|
126
|
-
} catch {
|
|
127
|
-
return fail($, { state: 'error', detail: 'Unhide could not be saved on this Mac.' })
|
|
128
|
-
}
|
|
129
|
-
await update($, cleared, ids => ids.filter(other => other !== id))
|
|
130
|
-
await recount($)
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/** Loads a Room the person opened: its threads with unread first, then the newest; per thread the newest 20 messages and every unread one (up to 100). */
|
|
134
|
-
export async function openRoom($: EngineInterface, roomId: string, consent: string): Promise<void> {
|
|
135
|
-
const shown = await callGateway($, 'work_room_read', { operation: 'show', room_id: roomId }, consent)
|
|
136
|
-
if (!shown.ok) return fail($, shown)
|
|
137
|
-
const info = parseRoom(findPayload(shown.payload, v => v.room !== undefined))
|
|
138
|
-
if (!info) return fail($, { state: 'error', detail: 'The Room is unavailable.' })
|
|
139
|
-
const rows: ThreadRow[] = []
|
|
140
|
-
let cursor: number | null = 0
|
|
141
|
-
for (let page = 0; page < 5 && cursor !== null; page++) {
|
|
142
|
-
const listed = await callGateway($, 'work_room_read', { operation: 'threads', room_id: roomId, after_sequence: cursor, limit: 100 }, consent)
|
|
143
|
-
if (!listed.ok) return fail($, listed)
|
|
144
|
-
const parsed = parseThreads(findPayload(listed.payload, v => Array.isArray(v.threads)), rows.length)
|
|
145
|
-
if (!parsed) break
|
|
146
|
-
rows.push(...parsed.rows)
|
|
147
|
-
cursor = parsed.next
|
|
148
|
-
}
|
|
149
|
-
const threads: OpenRoom['threads'] = []
|
|
150
|
-
for (const row of pickThreads(rows)) {
|
|
151
|
-
const latest = await callGateway($, 'work_room_read', { operation: 'messages', room_id: roomId, thread_id: row.id, latest: true, limit: 20 }, consent)
|
|
152
|
-
if (!latest.ok) return fail($, latest)
|
|
153
|
-
let unread: RoomMessage[] = []
|
|
154
|
-
if (row.unread > 0) {
|
|
155
|
-
const page = await callGateway($, 'work_room_read', { operation: 'messages', room_id: roomId, thread_id: row.id, unread_only: true, limit: 100 }, consent)
|
|
156
|
-
if (!page.ok) return fail($, page)
|
|
157
|
-
unread = parseMessages(findPayload(page.payload, v => Array.isArray(v.messages))) ?? []
|
|
158
|
-
}
|
|
159
|
-
const newest = parseMessages(findPayload(latest.payload, v => Array.isArray(v.messages))) ?? []
|
|
160
|
-
threads.push({ id: row.id, title: row.title, messages: mergeMessages(newest, unread), unreadIds: unread.map(m => m.id) })
|
|
161
|
-
}
|
|
162
|
-
await update($, rooms, all => ({ ...all, [roomId]: { info, threads } }))
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/** Closes a Room on this Mac only; opening it again loads its newest messages. */
|
|
166
|
-
async function closeRoom($: EngineInterface, roomId: string): Promise<void> {
|
|
167
|
-
await update($, rooms, all => {
|
|
168
|
-
const { [roomId]: _closed, ...rest } = all
|
|
169
|
-
|
|
170
|
-
return rest
|
|
171
|
-
})
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/** Reports presented once for messages the pane has drawn that the Gateway does not yet record as presented. */
|
|
175
|
-
export async function reportPresented($: EngineInterface, roomId: string, ids: string[]): Promise<void> {
|
|
176
|
-
const fresh = ids.filter(id => !presentedSent.has(id)).slice(0, 100)
|
|
177
|
-
if (fresh.length === 0) return
|
|
178
|
-
for (const id of fresh) presentedSent.add(id)
|
|
179
|
-
const name = (await read($, rooms))[roomId]?.info.name ?? 'a Room'
|
|
180
|
-
// The person's press on Open is what showed these messages, so the receipt carries it as consent.
|
|
181
|
-
const consent = `The user opened Room ${name} in the Work Room pane, which showed them ${fresh.length} message${fresh.length === 1 ? '' : 's'}.`
|
|
182
|
-
const sent = await callGateway($, 'work_room_receipt_report', { room_id: roomId, message_ids: fresh, state: 'presented' }, consent)
|
|
183
|
-
if (!sent.ok) {
|
|
184
|
-
for (const id of fresh) presentedSent.delete(id)
|
|
185
|
-
return fail($, sent)
|
|
186
|
-
}
|
|
187
|
-
const done = new Set(fresh)
|
|
188
|
-
await update($, rooms, all => {
|
|
189
|
-
const room = all[roomId]
|
|
190
|
-
if (!room) return all
|
|
191
|
-
|
|
192
|
-
return { ...all, [roomId]: { ...room, threads: room.threads.map(thread => ({ ...thread, messages: thread.messages.map(m => (done.has(m.id) ? { ...m, presented: true } : m)) })) } }
|
|
193
|
-
})
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/** Writes already confirmed in this run, by write key, so a second press of the same text sends once. */
|
|
197
|
-
const sent = new Set<string>()
|
|
198
|
-
|
|
199
|
-
const KIND_LABEL: Record<InboxItem['kind'], string> = { request: 'Request', decision: 'Decision', review: 'Review request' }
|
|
200
|
-
const ITEM_HINT = 'Ask Claude sums the request up here and sends nothing. Hide is only on this Mac; it stays open for the person who asked.'
|
|
201
|
-
|
|
202
|
-
async function stage($: EngineInterface, next: Pending): Promise<void> {
|
|
203
|
-
await update($, pending, () => next)
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
async function cancel($: EngineInterface): Promise<void> {
|
|
207
|
-
await update($, pending, () => null)
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/** Moving away from where a write was staged (another tab, a closed card or Room) drops it unsent. */
|
|
211
|
-
async function leave($: EngineInterface, move: () => Promise<unknown>): Promise<void> {
|
|
212
|
-
await cancel($)
|
|
213
|
-
await move()
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/** Sends the staged write. The person pressed confirm on its summary, which the call carries as consent. */
|
|
217
|
-
async function confirm($: EngineInterface): Promise<void> {
|
|
218
|
-
const job = await read($, pending)
|
|
219
|
-
if (!job) return
|
|
220
|
-
await update($, pending, () => null)
|
|
221
|
-
const key = writeKey(`${job.kind}:${job.target}`, job.text)
|
|
222
|
-
if (sent.has(key)) return
|
|
223
|
-
sent.add(key)
|
|
224
|
-
const consent = `The user confirmed "${job.summary}" in the Work Room pane.`
|
|
225
|
-
const [roomId = '', threadId = '', revision = '0', requestId = ''] = job.target.split('|')
|
|
226
|
-
const answer = job.kind === 'answer'
|
|
227
|
-
? await callGateway($, 'work_request_answer', { request_id: job.target, body: job.text }, consent)
|
|
228
|
-
: job.kind === 'post'
|
|
229
|
-
? await callGateway($, 'work_room_write', { operation: 'post', room_id: roomId, thread_id: threadId, body: job.text, membership_revision: Number(revision), idempotency_key: key }, consent)
|
|
230
|
-
: job.kind === 'answer_share'
|
|
231
|
-
? await callGateway($, 'work_room_write', { operation: 'answer_share', room_id: roomId, thread_id: threadId, request_id: requestId, membership_revision: Number(revision), idempotency_key: key }, consent)
|
|
232
|
-
: await callGateway($, 'work_room_receipt_report', { room_id: roomId, message_ids: job.text.split(',').slice(0, 100), state: 'user_acknowledged', human_confirmed: true }, consent)
|
|
233
|
-
if (!answer.ok) {
|
|
234
|
-
sent.delete(key)
|
|
235
|
-
return fail($, answer)
|
|
236
|
-
}
|
|
237
|
-
if (job.kind !== 'answer') await openRoom($, roomId, consent)
|
|
238
|
-
await refreshCounts($)
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
export const register: Register = on => {
|
|
242
|
-
on('session.start', async ($, e, next) => {
|
|
243
|
-
const started = await next(e)
|
|
244
|
-
await $.command.register({ name: 'work-room', description: 'Show Work Room shares and requests from colleagues' })
|
|
245
|
-
$.clock.every(REFRESH_MS, () => {
|
|
246
|
-
quietly(refreshCounts($))
|
|
247
|
-
})
|
|
248
|
-
quietly(refreshCounts($))
|
|
249
|
-
|
|
250
|
-
return started
|
|
251
|
-
})
|
|
252
|
-
|
|
253
|
-
on('turn.complete', async ($, e, next) => {
|
|
254
|
-
if (e.agentId === undefined) quietly(refreshCounts($))
|
|
255
|
-
|
|
256
|
-
return next(e)
|
|
257
|
-
})
|
|
258
|
-
|
|
259
|
-
on('command.run', { command: 'work-room' }, async $ => {
|
|
260
|
-
await $.ui.open({ id: PANE, title: TITLE })
|
|
261
|
-
quietly(refreshCounts($))
|
|
262
|
-
|
|
263
|
-
return { text: 'Work Room pane opened.' }
|
|
264
|
-
})
|
|
265
|
-
|
|
266
|
-
on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
|
|
267
|
-
const text = bandText(await read($, counts))
|
|
268
|
-
if (text === null) return next(e)
|
|
269
|
-
const { Box, Button, Text } = $.ui.resolve(e)
|
|
270
|
-
|
|
271
|
-
return (
|
|
272
|
-
<Box>
|
|
273
|
-
<Text>{text} </Text>
|
|
274
|
-
<Button key="work-room-open" label="Open" onPress={() => quietly($.ui.open({ id: PANE, title: TITLE }))} />
|
|
275
|
-
</Box>
|
|
276
|
-
)
|
|
277
|
-
})
|
|
278
|
-
|
|
279
|
-
on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
|
|
280
|
-
const table = $.ui.resolve(e)
|
|
281
|
-
const { Box, Button, Text } = table
|
|
282
|
-
// The mobile app draws no field yet: there the pane reads, shares and marks read, and answers go through Ask Claude.
|
|
283
|
-
const Input = 'Input' in table ? table.Input : undefined
|
|
284
|
-
const state = await read($, status)
|
|
285
|
-
const job = await read($, pending)
|
|
286
|
-
const asks = await read($, inbox)
|
|
287
|
-
const hidden = await read($, cleared)
|
|
288
|
-
const showHidden = await read($, showCleared)
|
|
289
|
-
const visible = visibleInbox(asks, hidden)
|
|
290
|
-
const clearedItems = asks.filter(item => hidden.includes(item.id))
|
|
291
|
-
const rows = await read($, activity)
|
|
292
|
-
const open = await read($, rooms)
|
|
293
|
-
const current = await read($, tab)
|
|
294
|
-
const now = await read($, nowMs)
|
|
295
|
-
const groups = groupInbox(visible)
|
|
296
|
-
const chosen = await read($, openGroup)
|
|
297
|
-
// Nothing chosen opens the newest group; '' is a group the person closed.
|
|
298
|
-
const openKey = typeof chosen === 'string' ? chosen : groups[0]?.key
|
|
299
|
-
const unreadTotal = rows.reduce((sum, row) => sum + row.unread, 0)
|
|
300
|
-
// The confirm row stands under the field or button that staged it; leaving that place drops the staged write.
|
|
301
|
-
const [jobRoom = '', jobThread = ''] = job && job.kind !== 'answer' ? job.target.split('|') : []
|
|
302
|
-
const answerOpen = job?.kind === 'answer' && current === 'requests' && groups.some(group => group.key === openKey && group.items[0]?.id === job.target)
|
|
303
|
-
const roomOpen = job !== null && job.kind !== 'answer' && current === 'rooms' && open[jobRoom] !== undefined
|
|
304
|
-
const inlinePlace = answerOpen ? `item:${job?.target}` : roomOpen ? (jobThread && job?.kind !== 'acknowledge' ? `thread:${jobThread}` : `room:${jobRoom}`) : null
|
|
305
|
-
const confirmRow = job && (
|
|
306
|
-
<Box flexDirection="row" flexWrap="wrap" gap="xs" alignItems="center">
|
|
307
|
-
<Text color="yellow">{job.summary}</Text>
|
|
308
|
-
<Box flexDirection="row" gap="xs">
|
|
309
|
-
<Button key="confirm" variant="primary" label="Confirm" onPress={() => quietly(confirm($))} />
|
|
310
|
-
<Button key="cancel" label="Cancel" onPress={() => quietly(cancel($))} />
|
|
311
|
-
</Box>
|
|
312
|
-
</Box>
|
|
313
|
-
)
|
|
314
|
-
const toReport: Array<[string, string[]]> = []
|
|
315
|
-
|
|
316
|
-
const view = (
|
|
317
|
-
<Box flexDirection="column">
|
|
318
|
-
{state.state !== 'ok' && <Text color="yellow">{state.detail}</Text>}
|
|
319
|
-
<Box flexDirection="row" gap="xs" marginBottom={1}>
|
|
320
|
-
<Button key="tab-requests" variant={current === 'requests' ? 'primary' : 'secondary'} label={`Requests · ${groups.length}`} onPress={() => quietly(leave($, () => update($, tab, () => 'requests')))} />
|
|
321
|
-
<Button key="tab-rooms" variant={current === 'rooms' ? 'primary' : 'secondary'} label={`Rooms · ${unreadTotal} unread`} onPress={() => quietly(leave($, () => update($, tab, () => 'rooms')))} />
|
|
322
|
-
</Box>
|
|
323
|
-
{current === 'requests' && (
|
|
324
|
-
<Box flexDirection="column" gap="xs">
|
|
325
|
-
{groups.length === 0 && <Text dimColor>No requests waiting for you.</Text>}
|
|
326
|
-
{groups.map(group => {
|
|
327
|
-
const item = group.items[0]!
|
|
328
|
-
const isOpen = group.key === openKey
|
|
329
|
-
|
|
330
|
-
return (
|
|
331
|
-
<Box flexDirection="column" gap="xs" borderStyle="round" borderColor={isOpen ? 'suggestion' : 'promptBorder'} paddingX={1}>
|
|
332
|
-
<Button key={`item-${item.id}`} onPress={() => quietly(leave($, () => update($, openGroup, () => (isOpen ? '' : group.key))))}>
|
|
333
|
-
<Text bold={isOpen} wrap={isOpen ? 'wrap' : 'truncate'}>{isOpen ? '▼ ' : '► '}{group.title}</Text>
|
|
334
|
-
</Button>
|
|
335
|
-
<Box paddingLeft={2}>
|
|
336
|
-
<Text dimColor size="footnote" wrap="truncate">{detailLine(item, group.items.length - 1, now)}</Text>
|
|
337
|
-
</Box>
|
|
338
|
-
{isOpen && item.kind === 'request' && Input && (
|
|
339
|
-
<Box flexDirection="column" paddingLeft={2} width="100%" flexGrow={1}>
|
|
340
|
-
<Input
|
|
341
|
-
key={`answer-${item.id}`}
|
|
342
|
-
placeholder="Answer this request"
|
|
343
|
-
rows={3}
|
|
344
|
-
submitLabel="Send"
|
|
345
|
-
onSubmit={(text: string) => quietly(stage($, { kind: 'answer', target: item.id, text, summary: `Answer goes to ${item.askedBy} only.` }))}
|
|
346
|
-
/>
|
|
347
|
-
</Box>
|
|
348
|
-
)}
|
|
349
|
-
{inlinePlace === `item:${item.id}` && <Box paddingLeft={2}>{confirmRow}</Box>}
|
|
350
|
-
{isOpen && (
|
|
351
|
-
<Box flexDirection="row" gap="xs" paddingLeft={2}>
|
|
352
|
-
<Button key={`chat-${item.id}`} label="Ask Claude" onPress={() => quietly($.prompt.submit({ text: chatPrompt(item) }))} />
|
|
353
|
-
<Button key={`hide-${item.id}`} label="Hide" onPress={() => quietly(Promise.all(group.items.map(each => clearItem($, each))))} />
|
|
354
|
-
</Box>
|
|
355
|
-
)}
|
|
356
|
-
{isOpen && (
|
|
357
|
-
<Box paddingLeft={2}>
|
|
358
|
-
<Text dimColor size="footnote">{ITEM_HINT}</Text>
|
|
359
|
-
</Box>
|
|
360
|
-
)}
|
|
361
|
-
</Box>
|
|
362
|
-
)
|
|
363
|
-
})}
|
|
364
|
-
{clearedItems.length > 0 && (
|
|
365
|
-
<Box flexDirection="row" gap="xs" alignItems="center" marginTop={1}>
|
|
366
|
-
<Text dimColor>Hidden ({clearedItems.length})</Text>
|
|
367
|
-
<Button key="show-hidden" label={showHidden ? 'Close' : 'Show'} onPress={() => quietly(update($, showCleared, shown => !shown))} />
|
|
368
|
-
</Box>
|
|
369
|
-
)}
|
|
370
|
-
{showHidden && clearedItems.map(item => (
|
|
371
|
-
<Box flexDirection="row" gap="xs" alignItems="center">
|
|
372
|
-
<Text dimColor>{KIND_LABEL[item.kind]} from {item.askedBy}: {item.title}</Text>
|
|
373
|
-
<Button key={`unhide-${item.id}`} label="Unhide" onPress={() => quietly(restoreItem($, item.id))} />
|
|
374
|
-
</Box>
|
|
375
|
-
))}
|
|
376
|
-
</Box>
|
|
377
|
-
)}
|
|
378
|
-
{current === 'rooms' && (
|
|
379
|
-
<Box flexDirection="column" gap="xs">
|
|
380
|
-
{rows.length === 0 && <Text dimColor>No Rooms.</Text>}
|
|
381
|
-
{rows.map(row => {
|
|
382
|
-
const room = open[row.roomId]
|
|
383
|
-
if (room) toReport.push([row.roomId, room.threads.flatMap(thread => thread.messages.filter(m => !m.presented).map(m => m.id))])
|
|
384
|
-
|
|
385
|
-
return (
|
|
386
|
-
<Box flexDirection="column" gap="xs" borderStyle="round" borderColor={room ? 'suggestion' : 'promptBorder'} paddingX={1}>
|
|
387
|
-
<Box flexDirection="row" justifyContent="space-between" alignItems="center" gap="xs">
|
|
388
|
-
<Button
|
|
389
|
-
key={`open-${row.roomId}`}
|
|
390
|
-
onPress={() => quietly(room ? leave($, () => closeRoom($, row.roomId)) : openRoom($, row.roomId, `The user pressed Open on Room ${row.name} in the Work Room pane.`))}
|
|
391
|
-
>
|
|
392
|
-
<Text bold={room !== undefined}>{room ? '▼ ' : '► '}{row.name} · {row.unread} unread</Text>
|
|
393
|
-
</Button>
|
|
394
|
-
<Box flexDirection="row" gap="xs">
|
|
395
|
-
{room && markable(room.threads).length > 0 && (
|
|
396
|
-
<Button
|
|
397
|
-
key={`ack-room-${row.roomId}`}
|
|
398
|
-
label="Mark read"
|
|
399
|
-
onPress={() => {
|
|
400
|
-
const ids = markable(room.threads)
|
|
401
|
-
quietly(stage($, { kind: 'acknowledge', target: `${row.roomId}|`, text: ids.join(','), summary: `Mark ${ids.length} message${ids.length === 1 ? '' : 's'} read?` }))
|
|
402
|
-
}}
|
|
403
|
-
/>
|
|
404
|
-
)}
|
|
405
|
-
</Box>
|
|
406
|
-
</Box>
|
|
407
|
-
{inlinePlace === `room:${row.roomId}` && confirmRow}
|
|
408
|
-
{room?.threads.map(thread => (
|
|
409
|
-
<Box flexDirection="column" gap="xs" paddingLeft={2}>
|
|
410
|
-
<Box flexDirection="row" justifyContent="space-between" alignItems="center" gap="xs">
|
|
411
|
-
<Text bold dimColor># {thread.title}</Text>
|
|
412
|
-
{room.threads.length > 1 && markable([thread]).length > 0 && (
|
|
413
|
-
<Button
|
|
414
|
-
key={`ack-${thread.id}`}
|
|
415
|
-
label="Mark read"
|
|
416
|
-
onPress={() => {
|
|
417
|
-
const ids = markable([thread])
|
|
418
|
-
quietly(stage($, { kind: 'acknowledge', target: `${row.roomId}|${thread.id}`, text: ids.join(','), summary: `Mark ${ids.length} message${ids.length === 1 ? '' : 's'} read?` }))
|
|
419
|
-
}}
|
|
420
|
-
/>
|
|
421
|
-
)}
|
|
422
|
-
</Box>
|
|
423
|
-
<Box flexDirection="column" gap="xs" paddingLeft={2}>
|
|
424
|
-
{thread.messages.map(m => (
|
|
425
|
-
<Box flexDirection="column">
|
|
426
|
-
<Text><Text bold>{authorName(room.info, m.author)}</Text>: {m.body}</Text>
|
|
427
|
-
{m.requestId !== undefined && (
|
|
428
|
-
<Button
|
|
429
|
-
key={`share-${m.id}`}
|
|
430
|
-
label="Share answer to Room"
|
|
431
|
-
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.` }))}
|
|
432
|
-
/>
|
|
433
|
-
)}
|
|
434
|
-
</Box>
|
|
435
|
-
))}
|
|
436
|
-
{Input && (
|
|
437
|
-
<Input
|
|
438
|
-
key={`post-${thread.id}`}
|
|
439
|
-
placeholder="Reply in this thread"
|
|
440
|
-
rows={2}
|
|
441
|
-
submitLabel="Send"
|
|
442
|
-
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.` }))}
|
|
443
|
-
/>
|
|
444
|
-
)}
|
|
445
|
-
{inlinePlace === `thread:${thread.id}` && confirmRow}
|
|
446
|
-
</Box>
|
|
447
|
-
</Box>
|
|
448
|
-
))}
|
|
449
|
-
</Box>
|
|
450
|
-
)
|
|
451
|
-
})}
|
|
452
|
-
</Box>
|
|
453
|
-
)}
|
|
454
|
-
</Box>
|
|
455
|
-
)
|
|
456
|
-
for (const [roomId, ids] of toReport) quietly(reportPresented($, roomId, ids))
|
|
457
|
-
|
|
458
|
-
return view
|
|
459
|
-
})
|
|
460
|
-
}
|
package/claude-mod/tsconfig.json
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
export type RoomActivity = { roomId: string; name: string; unread: number; latest: number }
|
|
2
|
-
export type InboxItem = { kind: 'request' | 'decision' | 'review'; id: string; workItemId: string; askedBy: string; title: string; status: string; changedAt: number }
|
|
3
|
-
export type RoomMessage = { id: string; threadId: string; sequence: number; author: string; kind: string; body: string; createdAt: number; presented: boolean; acknowledged: boolean; requestId?: string }
|
|
4
|
-
export type RoomInfo = { id: string; name: string; membershipRevision: number; memberCount: number; names: Record<string, string> }
|
|
5
|
-
export type ThreadRow = { id: string; title: string; unread: number; latest: number; order: number }
|
|
6
|
-
export type RoomThread = { id: string; title: string; messages: RoomMessage[]; unreadIds: string[] }
|
|
7
|
-
export type OpenRoom = { info: RoomInfo; threads: RoomThread[] }
|
|
8
|
-
export type Pending = { kind: 'answer' | 'post' | 'answer_share' | 'acknowledge'; target: string; text: string; summary: string } | null
|
|
9
|
-
export type Status = { state: 'ok' | 'no-connector' | 'refused' | 'error' | 'loading' | 'stale'; detail: string }
|
|
10
|
-
export type Counts = { shares: number; requests: number }
|
|
11
|
-
|
|
12
|
-
declare module 'claude-code' {
|
|
13
|
-
interface PluginState {
|
|
14
|
-
'ai-gateway-work-room': {
|
|
15
|
-
counts: Counts
|
|
16
|
-
activity: RoomActivity[]
|
|
17
|
-
inbox: InboxItem[]
|
|
18
|
-
rooms: Record<string, OpenRoom>
|
|
19
|
-
pending: Pending
|
|
20
|
-
status: Status
|
|
21
|
-
cleared: string[]
|
|
22
|
-
showCleared: boolean
|
|
23
|
-
tab: 'requests' | 'rooms'
|
|
24
|
-
openGroup: string | null
|
|
25
|
-
nowMs: number
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
}
|