claude-inbox 0.3.0 → 0.4.0
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.
- checksums.yaml +4 -4
- data/README.md +86 -164
- data/exe/claude-inbox +44 -0
- data/lib/claude_inbox/actions.rb +91 -0
- data/lib/claude_inbox/agents_client.rb +11 -7
- data/lib/claude_inbox/app.rb +48 -14
- data/lib/claude_inbox/headless.rb +31 -0
- data/lib/claude_inbox/job_state.rb +4 -4
- data/lib/claude_inbox/keymap.rb +0 -2
- data/lib/claude_inbox/mod.rb +71 -0
- data/lib/claude_inbox/new_session_form.rb +18 -17
- data/lib/claude_inbox/painter.rb +3 -4
- data/lib/claude_inbox/paste.rb +2 -0
- data/lib/claude_inbox/poller.rb +48 -25
- data/lib/claude_inbox/pull_requests.rb +31 -17
- data/lib/claude_inbox/rate_limits.rb +1 -3
- data/lib/claude_inbox/remote/http.rb +10 -7
- data/lib/claude_inbox/remote/listener.rb +2 -3
- data/lib/claude_inbox/remote/start.rb +2 -3
- data/lib/claude_inbox/renderer.rb +4 -4
- data/lib/claude_inbox/settings.rb +1 -0
- data/lib/claude_inbox/slash_commands.rb +1 -0
- data/lib/claude_inbox/snapshot.rb +48 -0
- data/lib/claude_inbox/store/entry.rb +0 -2
- data/lib/claude_inbox/store/row.rb +7 -3
- data/lib/claude_inbox/store.rb +24 -9
- data/lib/claude_inbox/switch.rb +40 -0
- data/lib/claude_inbox/text_buffer.rb +84 -33
- data/lib/claude_inbox/writer_lock.rb +65 -0
- data/lib/claude_inbox.rb +1 -10
- data/mod/.claude-plugin/plugin.json +20 -0
- data/mod/hooks/hooks.json +1 -0
- data/mod/hooks/register.tsx +470 -0
- data/mod/tests/contract.test.tsx +23 -0
- data/mod/tests/pane.test.tsx +199 -0
- data/mod/tests/snapshot.fixture.ts +47 -0
- data/mod/tests/snooze.test.tsx +31 -0
- data/mod/tsconfig.json +3 -0
- data/mod/types/index.d.ts +30 -0
- metadata +16 -1
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import type { On } from 'claude-code'
|
|
2
|
+
import { expect, mock, test } from 'claude-code/testing'
|
|
3
|
+
|
|
4
|
+
const PANE = {
|
|
5
|
+
plugin: 'inbox-pane',
|
|
6
|
+
component: 'Pane',
|
|
7
|
+
requestId: 'inbox',
|
|
8
|
+
props: { title: 'Inbox', isFocused: true, bodyColumns: 48, placement: 'dock', scroll: { offset: 0, bodyRows: 30 }, view: {} },
|
|
9
|
+
viewport: { columns: 160, rows: 40 },
|
|
10
|
+
} as const
|
|
11
|
+
|
|
12
|
+
const ROWS = [
|
|
13
|
+
{ section: 'needs_you', id: 'a1', label: 'needs an answer', state: 'blocked', actionable: true, line: 'confirm: drop it?' },
|
|
14
|
+
{ section: 'active', id: 'b2', label: 'still going', state: 'working', actionable: true, line: 'running bin/ci', pr: { short: '#7', state: 'open', url: 'https://github.com/o/r/pull/7' } },
|
|
15
|
+
{ section: 'active', id: 'uuid-term', label: 'a terminal', state: 'working', actionable: false, isTerminal: true },
|
|
16
|
+
{ section: 'settled', id: 'c3', label: 'put away', state: 'done', actionable: true },
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
// The op's result rides under `value`, so a seeded read is `{ value: { value, version } }`.
|
|
20
|
+
const seeded = (value: unknown) => ({ value: { value, version: 1 } })
|
|
21
|
+
|
|
22
|
+
// Answers what a session start asks of the engine, with a fresh snapshot so no headless
|
|
23
|
+
// inbox starts, and records the panes it opens and the state it sets. `polled` settles
|
|
24
|
+
// with the start's first poll.
|
|
25
|
+
const startable = (on: On, sections: Record<string, unknown[]> = {}) => {
|
|
26
|
+
const opened: string[] = []
|
|
27
|
+
const set: Record<string, unknown> = {}
|
|
28
|
+
let done = () => {}
|
|
29
|
+
const polled = new Promise<void>(resolve => (done = resolve))
|
|
30
|
+
on('session.start', ($, e) => ({ cwd: e.cwd }))
|
|
31
|
+
on('command.register', () => ({ value: undefined }))
|
|
32
|
+
on('session.id', () => ({ value: 'u-test' }))
|
|
33
|
+
on('fs.read', () => ({ value: JSON.stringify({ written_at: 1_700_000_000, sections }) }))
|
|
34
|
+
on('state.set', async ($, e, next) => {
|
|
35
|
+
set[e.key] = e.value
|
|
36
|
+
const result = await next(e)
|
|
37
|
+
if (e.key === 'writtenAt') done()
|
|
38
|
+
return result
|
|
39
|
+
})
|
|
40
|
+
on('ui.open', ($, e) => {
|
|
41
|
+
opened.push(e.id)
|
|
42
|
+
return { value: { isPlaced: true } }
|
|
43
|
+
})
|
|
44
|
+
return { opened, polled, set }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
test('without the gem the pane says so, on every surface that seats a pane', async ($, on) => {
|
|
48
|
+
mock.env(on, { HOME: '/Users/me' })
|
|
49
|
+
mock.clock(on, { now: 1_700_000_000_000 })
|
|
50
|
+
for (const surface of ['terminal', 'desktop', 'vscode', 'mobile'] as const) {
|
|
51
|
+
const ui = await $.ui.mount({ ...PANE, surface })
|
|
52
|
+
expect(await ui.find({ type: 'Text', text: /claude-inbox is not running/ })).toBeDefined()
|
|
53
|
+
await ui.unmount()
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('a snapshot older than thirty seconds is called stale, just now before a minute', async ($, on) => {
|
|
58
|
+
mock.env(on, { HOME: '/Users/me' })
|
|
59
|
+
mock.clock(on, { now: 1_700_000_000_000 })
|
|
60
|
+
on('state.get', ($, e, next) => (e.key === 'writtenAt' ? seeded(1_700_000_000 - 45) : next(e)))
|
|
61
|
+
const ui = await $.ui.mount({ ...PANE, surface: 'terminal' })
|
|
62
|
+
expect(await ui.find({ type: 'Text', text: /stopped just now; rows are stale/ })).toBeDefined()
|
|
63
|
+
await ui.unmount()
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test("rows sit under the gem's sections, Settled folded until za opens it", async ($, on) => {
|
|
67
|
+
mock.env(on, { HOME: '/Users/me' })
|
|
68
|
+
mock.clock(on, { now: 1_700_000_000_000 })
|
|
69
|
+
on('state.get', ($, e, next) => {
|
|
70
|
+
if (e.key === 'rows') return seeded(ROWS)
|
|
71
|
+
if (e.key === 'writtenAt') return seeded(1_700_000_000)
|
|
72
|
+
return next(e)
|
|
73
|
+
})
|
|
74
|
+
for (const surface of ['terminal', 'desktop'] as const) {
|
|
75
|
+
const ui = await $.ui.mount({ ...PANE, surface })
|
|
76
|
+
expect(await ui.find({ type: 'Text', text: /Needs you · 1/ })).toBeDefined()
|
|
77
|
+
expect(await ui.find({ type: 'Text', text: /Active · 2/ })).toBeDefined()
|
|
78
|
+
expect(await ui.find({ type: 'Text', text: /Settled · 1/ })).toBeDefined()
|
|
79
|
+
expect(await ui.find({ type: 'Text', text: /confirm: drop it\?/ })).toBeDefined()
|
|
80
|
+
expect(await ui.find({ type: 'Text', text: /#7 open/ })).toBeDefined()
|
|
81
|
+
expect(await ui.find({ type: 'Text', text: /1 folded/ })).toBeDefined()
|
|
82
|
+
expect(await ui.find({ type: 'Text', text: /put away/ })).toBeUndefined()
|
|
83
|
+
await ui.press({ key: 'g' })
|
|
84
|
+
await ui.press({ key: 'e' })
|
|
85
|
+
await ui.press({ key: 'z' })
|
|
86
|
+
expect(await ui.find({ type: 'Button', key: 'x' })).toBeUndefined()
|
|
87
|
+
await ui.press({ key: 'a' })
|
|
88
|
+
expect(await ui.find({ type: 'Text', text: /put away/ })).toBeDefined()
|
|
89
|
+
expect(await ui.find({ type: 'Button', key: 'x' })).toBeDefined()
|
|
90
|
+
await ui.press({ key: 'z' })
|
|
91
|
+
await ui.press({ key: 'c' })
|
|
92
|
+
expect(await ui.find({ type: 'Text', text: /put away/ })).toBeUndefined()
|
|
93
|
+
// Cursor and folds live in session state, so put them back for the next surface.
|
|
94
|
+
await ui.press({ key: 'g' })
|
|
95
|
+
await ui.press({ key: 'g' })
|
|
96
|
+
await ui.unmount()
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
test('the session the pane sits in leads the list under This session, with its own keys', async ($, on) => {
|
|
101
|
+
mock.env(on, { HOME: '/Users/me' })
|
|
102
|
+
mock.clock(on, { now: 1_700_000_000_000 })
|
|
103
|
+
const [viewing, ...others] = ROWS
|
|
104
|
+
on('state.get', ($, e, next) => {
|
|
105
|
+
if (e.key === 'rows') return seeded(others)
|
|
106
|
+
if (e.key === 'current') return seeded(viewing)
|
|
107
|
+
if (e.key === 'writtenAt') return seeded(1_700_000_000)
|
|
108
|
+
return next(e)
|
|
109
|
+
})
|
|
110
|
+
const written: string[] = []
|
|
111
|
+
on('fs.write', ($, e) => {
|
|
112
|
+
written.push(e.path)
|
|
113
|
+
return { value: undefined }
|
|
114
|
+
})
|
|
115
|
+
const ui = await $.ui.mount({ ...PANE, surface: 'terminal' })
|
|
116
|
+
expect(await ui.find({ type: 'Text', text: /^This session$/ })).toBeDefined()
|
|
117
|
+
expect(await ui.find({ type: 'Text', text: /needs an answer/ })).toBeDefined()
|
|
118
|
+
expect(await ui.find({ type: 'Text', text: /Needs you/ })).toBeUndefined()
|
|
119
|
+
await ui.press({ key: 'row:a1' })
|
|
120
|
+
expect(written).toEqual([])
|
|
121
|
+
await ui.press({ key: 'x' })
|
|
122
|
+
expect(written).toEqual(['/Users/me/.config/claude-inbox/actions/pane-1700000000000.json'])
|
|
123
|
+
await ui.unmount()
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
test('a poll lifts the row whose session is this one out of the list', async ($, on) => {
|
|
127
|
+
mock.env(on, { HOME: '/Users/me' })
|
|
128
|
+
mock.clock(on, { now: 1_700_000_000_000 })
|
|
129
|
+
const { polled, set } = startable(on, {
|
|
130
|
+
needs_you: [{ id: 'a1', session: 'u-test', label: 'mine' }],
|
|
131
|
+
active: [{ id: 'b2', session: 'u-b2', label: 'other' }],
|
|
132
|
+
})
|
|
133
|
+
await $.session.start({ cwd: '/tmp/proj', surface: 'terminal', isInteractive: true })
|
|
134
|
+
await polled
|
|
135
|
+
expect((set.current as { id?: string } | null)?.id).toBe('a1')
|
|
136
|
+
expect((set.rows as { id?: string }[]).map(row => row.id)).toEqual(['b2'])
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
test('x leaves a settle request for the inbox to apply', async ($, on) => {
|
|
140
|
+
mock.env(on, { HOME: '/Users/me' })
|
|
141
|
+
mock.clock(on, { now: 1_700_000_000_000 })
|
|
142
|
+
on('state.get', ($, e, next) => (e.key === 'rows' ? seeded(ROWS) : e.key === 'writtenAt' ? seeded(1_700_000_000) : next(e)))
|
|
143
|
+
const written: { path: string; text: string }[] = []
|
|
144
|
+
on('fs.write', ($, e) => {
|
|
145
|
+
written.push({ path: e.path, text: e.text })
|
|
146
|
+
return { value: undefined }
|
|
147
|
+
})
|
|
148
|
+
const ui = await $.ui.mount({ ...PANE, surface: 'terminal' })
|
|
149
|
+
await ui.press({ key: 'x' })
|
|
150
|
+
expect(written.length).toBe(1)
|
|
151
|
+
expect(written[0]?.path).toBe('/Users/me/.config/claude-inbox/actions/pane-1700000000000.json')
|
|
152
|
+
expect(JSON.parse(written[0]?.text ?? '{}')).toEqual({ action: 'settle', id: 'a1', at: 1_700_000_000_000 })
|
|
153
|
+
await ui.unmount()
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
test('Enter asks the attached inbox for the row, naming this session, and refuses a terminal', async ($, on) => {
|
|
157
|
+
mock.env(on, { HOME: '/Users/me' })
|
|
158
|
+
mock.clock(on, { now: 1_700_000_000_000 })
|
|
159
|
+
on('state.get', ($, e, next) => (e.key === 'rows' ? seeded(ROWS) : e.key === 'writtenAt' ? seeded(1_700_000_000) : next(e)))
|
|
160
|
+
const written: { path: string; text: string }[] = []
|
|
161
|
+
on('fs.write', ($, e) => {
|
|
162
|
+
written.push({ path: e.path, text: e.text })
|
|
163
|
+
return { value: undefined }
|
|
164
|
+
})
|
|
165
|
+
const ui = await $.ui.mount({ ...PANE, surface: 'terminal' })
|
|
166
|
+
await ui.press({ key: 'row:uuid-term' })
|
|
167
|
+
expect(written.length).toBe(0)
|
|
168
|
+
await ui.press({ key: 'row:b2' })
|
|
169
|
+
expect(written.length).toBe(1)
|
|
170
|
+
expect(written[0]?.path).toBe('/Users/me/.config/claude-inbox/switch.json')
|
|
171
|
+
expect(JSON.parse(written[0]?.text ?? '{}')).toEqual({ id: 'b2', from: 'pane', at: 1_700_000_000 })
|
|
172
|
+
await ui.unmount()
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
test('an unfocused pane says how to focus it', async ($, on) => {
|
|
176
|
+
mock.env(on, { HOME: '/Users/me' })
|
|
177
|
+
mock.clock(on, { now: 1_700_000_000_000 })
|
|
178
|
+
const ui = await $.ui.mount({ ...PANE, surface: 'terminal', props: { ...PANE.props, isFocused: false } })
|
|
179
|
+
expect(await ui.find({ type: 'Text', text: /takes you here/ })).toBeDefined()
|
|
180
|
+
await ui.unmount()
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
test('the pane opens when a session starts', async ($, on) => {
|
|
184
|
+
mock.env(on, { HOME: '/Users/me' })
|
|
185
|
+
mock.clock(on, { now: 1_700_000_000_000 })
|
|
186
|
+
const { opened, polled } = startable(on)
|
|
187
|
+
await $.session.start({ cwd: '/tmp/proj', surface: 'terminal', isInteractive: true })
|
|
188
|
+
await polled
|
|
189
|
+
expect(opened).toEqual(['inbox'])
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
test('with openOnStart off only /inbox opens it', { options: { openOnStart: false } }, async ($, on) => {
|
|
193
|
+
mock.env(on, { HOME: '/Users/me' })
|
|
194
|
+
mock.clock(on, { now: 1_700_000_000_000 })
|
|
195
|
+
const { opened, polled } = startable(on)
|
|
196
|
+
await $.session.start({ cwd: '/tmp/proj', surface: 'terminal', isInteractive: true })
|
|
197
|
+
await polled
|
|
198
|
+
expect(opened).toEqual([])
|
|
199
|
+
})
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// What the gem's Snapshot writes for three sessions, a blocked one, a working one
|
|
2
|
+
// with an open pull request and a hand-settled one. The gem's spec/mod_spec.rb
|
|
3
|
+
// writes the same sessions through Snapshot and compares them to this text, so a
|
|
4
|
+
// key renamed on either side fails a test on both.
|
|
5
|
+
export const SNAPSHOT = `{
|
|
6
|
+
"version": 1,
|
|
7
|
+
"written_at": 1789600000,
|
|
8
|
+
"sections": {
|
|
9
|
+
"pinned": [],
|
|
10
|
+
"needs_you": [
|
|
11
|
+
{
|
|
12
|
+
"id": "a1",
|
|
13
|
+
"session": "u-a1",
|
|
14
|
+
"label": "needs an answer",
|
|
15
|
+
"state": "blocked",
|
|
16
|
+
"actionable": true,
|
|
17
|
+
"cwd": "/tmp/proj"
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
"active": [
|
|
21
|
+
{
|
|
22
|
+
"id": "b2",
|
|
23
|
+
"session": "u-b2",
|
|
24
|
+
"label": "still going",
|
|
25
|
+
"state": "working",
|
|
26
|
+
"actionable": true,
|
|
27
|
+
"cwd": "/tmp/proj",
|
|
28
|
+
"pr": {
|
|
29
|
+
"short": "#7",
|
|
30
|
+
"state": "open",
|
|
31
|
+
"url": "https://github.com/o/r/pull/7"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
],
|
|
35
|
+
"snoozed": [],
|
|
36
|
+
"settled": [
|
|
37
|
+
{
|
|
38
|
+
"id": "c3",
|
|
39
|
+
"session": "u-c3",
|
|
40
|
+
"label": "put away",
|
|
41
|
+
"state": "done",
|
|
42
|
+
"actionable": true,
|
|
43
|
+
"cwd": "/tmp/proj"
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
}`
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { expect, mock, test } from 'claude-code/testing'
|
|
2
|
+
|
|
3
|
+
const PANE = {
|
|
4
|
+
plugin: 'inbox-pane',
|
|
5
|
+
component: 'Pane',
|
|
6
|
+
requestId: 'inbox',
|
|
7
|
+
props: { title: 'Inbox', isFocused: true, bodyColumns: 48, placement: 'dock', scroll: { offset: 0, bodyRows: 30 }, view: {} },
|
|
8
|
+
viewport: { columns: 160, rows: 40 },
|
|
9
|
+
} as const
|
|
10
|
+
const ROWS = [{ section: 'needs_you', id: 'a1', label: 'needs an answer', state: 'blocked', actionable: true, line: 'confirm: drop it?' }]
|
|
11
|
+
const seeded = (value: unknown) => ({ value: { value, version: 1 } })
|
|
12
|
+
|
|
13
|
+
test('s then 1 leaves a fifteen-minute snooze request', async ($, on) => {
|
|
14
|
+
mock.env(on, { HOME: '/Users/me' })
|
|
15
|
+
mock.clock(on, { now: 1_700_000_000_000 })
|
|
16
|
+
on('state.get', ($, e, next) => (e.key === 'rows' ? seeded(ROWS) : e.key === 'writtenAt' ? seeded(1_700_000_000) : next(e)))
|
|
17
|
+
const written: string[] = []
|
|
18
|
+
on('fs.write', ($, e) => {
|
|
19
|
+
written.push(e.text)
|
|
20
|
+
return { value: undefined }
|
|
21
|
+
})
|
|
22
|
+
const ui = await $.ui.mount({ ...PANE, surface: 'terminal' })
|
|
23
|
+
expect(await ui.find({ type: 'Button', key: '1' })).toBeUndefined()
|
|
24
|
+
await ui.press({ key: 's' })
|
|
25
|
+
expect(await ui.find({ type: 'Button', key: '1' })).toBeDefined()
|
|
26
|
+
expect(await ui.find({ type: 'Text', text: /15 minutes/ })).toBeDefined()
|
|
27
|
+
await ui.press({ key: '1' })
|
|
28
|
+
expect(written.length).toBe(1)
|
|
29
|
+
expect(JSON.parse(written[0] ?? '{}')).toEqual({ action: 'snooze', id: 'a1', choice: 'm15', at: 1_700_000_000_000 })
|
|
30
|
+
await ui.unmount()
|
|
31
|
+
})
|
data/mod/tsconfig.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export type Section = 'pinned' | 'needs_you' | 'active' | 'snoozed' | 'settled'
|
|
2
|
+
|
|
3
|
+
// One row of the gem's snapshot.json, as its Snapshot class writes it.
|
|
4
|
+
export type InboxRow = {
|
|
5
|
+
section: Section
|
|
6
|
+
id?: string
|
|
7
|
+
// The daemon's sessionId, the name $.session.id() gives the session the pane sits in.
|
|
8
|
+
session?: string
|
|
9
|
+
label: string
|
|
10
|
+
state: string
|
|
11
|
+
actionable: boolean
|
|
12
|
+
isWaiting: boolean
|
|
13
|
+
isTerminal: boolean
|
|
14
|
+
cwd?: string
|
|
15
|
+
line?: string
|
|
16
|
+
wakeAt?: number | 'until_woken'
|
|
17
|
+
remote?: string
|
|
18
|
+
pr?: { short: string; state: string; url: string }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type Folds = { snoozed: boolean; settled: boolean }
|
|
22
|
+
|
|
23
|
+
// The first key of a chord (g, z, s) waiting for its second; null between chords.
|
|
24
|
+
export type Pending = 'g' | 'z' | 's' | null
|
|
25
|
+
|
|
26
|
+
declare module 'claude-code' {
|
|
27
|
+
interface PluginState {
|
|
28
|
+
'inbox-pane': { rows: InboxRow[]; current: InboxRow | null; writtenAt: number; cursor: number; folds: Folds; pending: Pending }
|
|
29
|
+
}
|
|
30
|
+
}
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: claude-inbox
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jordan Byron
|
|
@@ -105,15 +105,18 @@ files:
|
|
|
105
105
|
- README.md
|
|
106
106
|
- exe/claude-inbox
|
|
107
107
|
- lib/claude_inbox.rb
|
|
108
|
+
- lib/claude_inbox/actions.rb
|
|
108
109
|
- lib/claude_inbox/agents_client.rb
|
|
109
110
|
- lib/claude_inbox/app.rb
|
|
110
111
|
- lib/claude_inbox/config.rb
|
|
111
112
|
- lib/claude_inbox/debug.rb
|
|
112
113
|
- lib/claude_inbox/dialog.rb
|
|
114
|
+
- lib/claude_inbox/headless.rb
|
|
113
115
|
- lib/claude_inbox/images.rb
|
|
114
116
|
- lib/claude_inbox/job_state.rb
|
|
115
117
|
- lib/claude_inbox/keymap.rb
|
|
116
118
|
- lib/claude_inbox/logs.rb
|
|
119
|
+
- lib/claude_inbox/mod.rb
|
|
117
120
|
- lib/claude_inbox/mouse.rb
|
|
118
121
|
- lib/claude_inbox/new_session_form.rb
|
|
119
122
|
- lib/claude_inbox/painter.rb
|
|
@@ -140,18 +143,30 @@ files:
|
|
|
140
143
|
- lib/claude_inbox/sessions.rb
|
|
141
144
|
- lib/claude_inbox/settings.rb
|
|
142
145
|
- lib/claude_inbox/slash_commands.rb
|
|
146
|
+
- lib/claude_inbox/snapshot.rb
|
|
143
147
|
- lib/claude_inbox/store.rb
|
|
144
148
|
- lib/claude_inbox/store/entry.rb
|
|
145
149
|
- lib/claude_inbox/store/row.rb
|
|
146
150
|
- lib/claude_inbox/store/sections.rb
|
|
147
151
|
- lib/claude_inbox/store/selection.rb
|
|
148
152
|
- lib/claude_inbox/subprocess.rb
|
|
153
|
+
- lib/claude_inbox/switch.rb
|
|
149
154
|
- lib/claude_inbox/terminal.rb
|
|
150
155
|
- lib/claude_inbox/text.rb
|
|
151
156
|
- lib/claude_inbox/text_buffer.rb
|
|
152
157
|
- lib/claude_inbox/theme.rb
|
|
153
158
|
- lib/claude_inbox/trust.rb
|
|
154
159
|
- lib/claude_inbox/vt_screen.rb
|
|
160
|
+
- lib/claude_inbox/writer_lock.rb
|
|
161
|
+
- mod/.claude-plugin/plugin.json
|
|
162
|
+
- mod/hooks/hooks.json
|
|
163
|
+
- mod/hooks/register.tsx
|
|
164
|
+
- mod/tests/contract.test.tsx
|
|
165
|
+
- mod/tests/pane.test.tsx
|
|
166
|
+
- mod/tests/snapshot.fixture.ts
|
|
167
|
+
- mod/tests/snooze.test.tsx
|
|
168
|
+
- mod/tsconfig.json
|
|
169
|
+
- mod/types/index.d.ts
|
|
155
170
|
homepage: https://github.com/jordanbyron/claude-inbox
|
|
156
171
|
licenses:
|
|
157
172
|
- MIT
|