claude-office 0.1.0 → 0.1.1
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 +103 -0
- data/lib/claude_office/rendering/renderer.rb +109 -67
- data/lib/claude_office/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 38dc036ecf1211a0ddaf31b55de675406374bc638a15e5dd1d8637563cb12110
|
|
4
|
+
data.tar.gz: da153e1ebe077536daf3e50b6c9161085760d10ac7bff971d843dfb3d60d3bd2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 98a4909b569b21a10633d4c053e05096ce556c8e419e747de9713a38d1ed9503314d95b6c4a85ff4c42d986897231336d1830b4f5049706fd48fbba9d446c1df
|
|
7
|
+
data.tar.gz: fdc2216861aae2e25f424ae9a5b0d5897206b7d27019da8bdfc57871146e356cb0a18a37a9b70d1adb995b03b60e2d420c8594b8c7f48c1c3e1b5c0561beb679
|
data/README.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# claude-office
|
|
2
|
+
|
|
3
|
+
A TUI companion for [Claude Code](https://docs.anthropic.com/en/docs/claude-code) — watch your AI coding sessions come alive as animated kaomoji characters in a virtual terminal office.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
┌─ claude-office ─────────────────────────────────────────────┐
|
|
7
|
+
│ │
|
|
8
|
+
│ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │
|
|
9
|
+
│ ░░ ┌─────┐ ┌─────┐ ┌─────┐ ░░ │
|
|
10
|
+
│ ░░ │ ▒▒▒ │ │ ▒▒▒ │ │ ▒▒▒ │ ░░ │
|
|
11
|
+
│ ░░ └──┬──┘ └──┬──┘ └──┬──┘ ░░ │
|
|
12
|
+
│ ░░ (o.o)~ (o.O) (-.-)zzZ ░░ │
|
|
13
|
+
│ ░░ "Edit app.rb" "Reading tests" ░░ │
|
|
14
|
+
│ ░░ └─ (o_o) ░░ │
|
|
15
|
+
│ ░░ "Sub: search" ░░ │
|
|
16
|
+
│ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │
|
|
17
|
+
│ │
|
|
18
|
+
├──────────────────────────────────────────────────────────────┤
|
|
19
|
+
│ Agent 1: typing (1 sub) │ Agent 2: reading │ q: quit │
|
|
20
|
+
└──────────────────────────────────────────────────────────────┘
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## What it does
|
|
24
|
+
|
|
25
|
+
claude-office reads Claude Code's JSONL transcript files in real-time and renders an animated office where each Claude session is a kaomoji character sitting at a desk. Characters change expressions based on what tools Claude is using:
|
|
26
|
+
|
|
27
|
+
| Expression | State | Meaning |
|
|
28
|
+
|-----------|-------|---------|
|
|
29
|
+
| `(o_o)` | idle | Waiting between actions |
|
|
30
|
+
| `(o.o)~` | typing | Editing or writing files |
|
|
31
|
+
| `(o.O)` | reading | Reading files, searching code |
|
|
32
|
+
| `(>.<)` | running | Executing bash commands |
|
|
33
|
+
| `(-.-)zzZ` | waiting | Turn ended, needs your input |
|
|
34
|
+
|
|
35
|
+
Sub-agents spawned by the `Task` tool appear indented below their parent with a tree connector.
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
gem install claude-office
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**Requirements:** Ruby 3.2+
|
|
44
|
+
|
|
45
|
+
## Usage
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# Watch Claude sessions in the current directory
|
|
49
|
+
claude-office
|
|
50
|
+
|
|
51
|
+
# Watch a specific project
|
|
52
|
+
claude-office ~/workspace/my-project
|
|
53
|
+
|
|
54
|
+
# Disable terminal bell notifications
|
|
55
|
+
claude-office --no-sound
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Run this in a **separate terminal window** alongside your Claude Code session. The office updates in real-time at 30fps as Claude works.
|
|
59
|
+
|
|
60
|
+
Press `q` to quit.
|
|
61
|
+
|
|
62
|
+
## How it works
|
|
63
|
+
|
|
64
|
+
1. Claude Code writes JSONL transcripts to `~/.claude/projects/<project-slug>/`
|
|
65
|
+
2. claude-office polls that directory for new `.jsonl` files
|
|
66
|
+
3. Each file becomes an agent (kaomoji character) at a desk
|
|
67
|
+
4. Tool use events (`Read`, `Edit`, `Bash`, etc.) drive character animations
|
|
68
|
+
5. Sub-agents from `Task` tool calls appear as child characters
|
|
69
|
+
6. When a turn ends, the character enters a waiting state with a speech bubble
|
|
70
|
+
|
|
71
|
+
## Architecture
|
|
72
|
+
|
|
73
|
+
Built with the [Charm Ruby](https://github.com/nicholaides/charm-ruby) ecosystem:
|
|
74
|
+
|
|
75
|
+
- **[bubbletea](https://rubygems.org/gems/bubbletea)** — Elm Architecture TUI framework
|
|
76
|
+
- **[lipgloss](https://rubygems.org/gems/lipgloss)** — Terminal styling and layout
|
|
77
|
+
- **[harmonica](https://rubygems.org/gems/harmonica)** — Spring-based animation physics
|
|
78
|
+
|
|
79
|
+
### Components
|
|
80
|
+
|
|
81
|
+
| Component | Purpose |
|
|
82
|
+
|-----------|---------|
|
|
83
|
+
| `Transcript::Watcher` | Background thread polling JSONL files |
|
|
84
|
+
| `Transcript::Parser` | Converts JSONL lines into typed events |
|
|
85
|
+
| `Agents::Agent` | State machine: idle / working / waiting |
|
|
86
|
+
| `Agents::SubAgent` | Child agent tracking for Task tool |
|
|
87
|
+
| `Office::Grid` | 2D tile grid with auto-layout |
|
|
88
|
+
| `Office::Pathfinder` | BFS pathfinding for character movement |
|
|
89
|
+
| `Rendering::Renderer` | Composites grid + characters + status bar |
|
|
90
|
+
| `Animation::SpringMover` | Smooth position interpolation |
|
|
91
|
+
|
|
92
|
+
## Development
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
git clone https://github.com/fruizg0302/claude-office.git
|
|
96
|
+
cd claude-office
|
|
97
|
+
bundle install
|
|
98
|
+
bundle exec rspec
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## License
|
|
102
|
+
|
|
103
|
+
[MIT](LICENSE)
|
|
@@ -11,75 +11,137 @@ module ClaudeOffice
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def render(grid:, agents:, frame:)
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
# Build a plain-text 2D buffer, then style entire lines
|
|
15
|
+
buffer = build_buffer(grid)
|
|
16
|
+
|
|
17
|
+
# Place desks into buffer
|
|
18
|
+
grid.desks.each do |desk|
|
|
19
|
+
place_desk(buffer, desk)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Place agents into buffer
|
|
23
|
+
agent_labels = []
|
|
24
|
+
agents.each do |agent|
|
|
25
|
+
place_agent(buffer, agent, frame, agent_labels)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Render buffer to styled string
|
|
29
|
+
title = render_title
|
|
30
|
+
office = render_buffer(buffer)
|
|
31
|
+
labels = render_labels(agent_labels)
|
|
16
32
|
status = render_status_bar(agents)
|
|
17
33
|
|
|
18
|
-
|
|
34
|
+
parts = [title, office]
|
|
35
|
+
parts << labels unless labels.empty?
|
|
36
|
+
parts << status
|
|
37
|
+
|
|
38
|
+
Lipgloss.join_vertical(:left, *parts)
|
|
19
39
|
end
|
|
20
40
|
|
|
21
41
|
private
|
|
22
42
|
|
|
23
|
-
def
|
|
24
|
-
|
|
25
|
-
.
|
|
26
|
-
|
|
43
|
+
def build_buffer(grid)
|
|
44
|
+
Array.new(grid.height) do |y|
|
|
45
|
+
Array.new(grid.width) do |x|
|
|
46
|
+
case grid.tiles[y][x]
|
|
47
|
+
when :wall then { char: Sprites::WALL_CHAR, type: :wall }
|
|
48
|
+
else { char: Sprites::FLOOR_CHAR, type: :floor }
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
27
52
|
end
|
|
28
53
|
|
|
29
|
-
def
|
|
30
|
-
|
|
54
|
+
def place_desk(buffer, desk)
|
|
55
|
+
dx, dy = desk.position
|
|
56
|
+
Sprites::DESK.each_with_index do |row, row_i|
|
|
57
|
+
row.chars.each_with_index do |ch, col_i|
|
|
58
|
+
bx = dx + col_i
|
|
59
|
+
by = dy + row_i
|
|
60
|
+
next if by < 0 || by >= buffer.length
|
|
61
|
+
next if bx < 0 || bx >= buffer[0].length
|
|
31
62
|
|
|
32
|
-
|
|
33
|
-
line = ""
|
|
34
|
-
grid.width.times do |x|
|
|
35
|
-
case grid.tiles[y][x]
|
|
36
|
-
when :wall
|
|
37
|
-
line += Theme::WALL_STYLE.render(Sprites::WALL_CHAR)
|
|
38
|
-
else
|
|
39
|
-
line += Theme::FLOOR_STYLE.render(Sprites::FLOOR_CHAR)
|
|
40
|
-
end
|
|
63
|
+
buffer[by][bx] = { char: ch, type: :desk }
|
|
41
64
|
end
|
|
42
|
-
lines << line
|
|
43
65
|
end
|
|
66
|
+
end
|
|
44
67
|
|
|
45
|
-
|
|
68
|
+
def place_agent(buffer, agent, frame, agent_labels)
|
|
69
|
+
pos = agent.desk_position
|
|
70
|
+
face = Sprites.face_for(agent.animation, frame: frame)
|
|
71
|
+
char_x = pos[0] + 1
|
|
72
|
+
char_y = pos[1] + Sprites::DESK.length + 1
|
|
46
73
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
74
|
+
# Place face characters into buffer
|
|
75
|
+
face.chars.each_with_index do |ch, i|
|
|
76
|
+
bx = char_x + i
|
|
77
|
+
next if char_y < 0 || char_y >= buffer.length
|
|
78
|
+
next if bx < 0 || bx >= buffer[0].length
|
|
79
|
+
|
|
80
|
+
buffer[char_y][bx] = { char: ch, type: :agent, state: agent.state }
|
|
50
81
|
end
|
|
51
82
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
agent_str = style.render(face)
|
|
83
|
+
# Collect label info for rendering below the grid
|
|
84
|
+
label_parts = []
|
|
85
|
+
label_parts << agent.status_text unless agent.status_text.empty?
|
|
56
86
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
87
|
+
agent.sub_agents.each_value do |sub|
|
|
88
|
+
sub_face = Sprites.sub_face_for(sub.animation)
|
|
89
|
+
sub_text = " └─ #{sub_face}"
|
|
90
|
+
sub_text += " \"#{sub.status_text}\"" unless sub.status_text.empty?
|
|
91
|
+
label_parts << sub_text
|
|
92
|
+
end
|
|
61
93
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
94
|
+
if agent.state == :waiting
|
|
95
|
+
label_parts.unshift("Needs input!")
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
unless label_parts.empty?
|
|
99
|
+
agent_labels << { state: agent.state, parts: label_parts }
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def render_title
|
|
104
|
+
Theme::TITLE_STYLE
|
|
105
|
+
.width(@width)
|
|
106
|
+
.render("claude-office")
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def render_buffer(buffer)
|
|
110
|
+
lines = buffer.map do |row|
|
|
111
|
+
row.map do |cell|
|
|
112
|
+
case cell[:type]
|
|
113
|
+
when :wall
|
|
114
|
+
Theme::WALL_STYLE.render(cell[:char])
|
|
115
|
+
when :desk
|
|
116
|
+
Theme::DESK_STYLE.render(cell[:char])
|
|
117
|
+
when :agent
|
|
118
|
+
Theme.agent_style(cell[:state]).render(cell[:char])
|
|
119
|
+
else
|
|
120
|
+
Theme::FLOOR_STYLE.render(cell[:char])
|
|
67
121
|
end
|
|
68
|
-
|
|
69
|
-
|
|
122
|
+
end.join
|
|
123
|
+
end
|
|
70
124
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
125
|
+
lines.join("\n")
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def render_labels(agent_labels)
|
|
129
|
+
return "" if agent_labels.empty?
|
|
75
130
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
131
|
+
lines = agent_labels.map do |info|
|
|
132
|
+
style = Theme.agent_style(info[:state])
|
|
133
|
+
info[:parts].map do |part|
|
|
134
|
+
if part.start_with?(" └─")
|
|
135
|
+
Theme::SUB_AGENT_STYLE.render(part)
|
|
136
|
+
elsif part == "Needs input!"
|
|
137
|
+
Theme::WAITING_AGENT_STYLE.render("⚡ #{part}")
|
|
138
|
+
else
|
|
139
|
+
Theme::STATUS_TEXT_STYLE.render(" \"#{part}\"")
|
|
140
|
+
end
|
|
141
|
+
end.join("\n")
|
|
80
142
|
end
|
|
81
143
|
|
|
82
|
-
|
|
144
|
+
lines.join("\n")
|
|
83
145
|
end
|
|
84
146
|
|
|
85
147
|
def render_status_bar(agents)
|
|
@@ -97,26 +159,6 @@ module ClaudeOffice
|
|
|
97
159
|
.width(@width)
|
|
98
160
|
.render(bar_text)
|
|
99
161
|
end
|
|
100
|
-
|
|
101
|
-
def overlay(base, overlay_str, x, y)
|
|
102
|
-
base_lines = base.split("\n")
|
|
103
|
-
overlay_lines = overlay_str.split("\n")
|
|
104
|
-
|
|
105
|
-
overlay_lines.each_with_index do |oline, i|
|
|
106
|
-
target_y = y + i
|
|
107
|
-
next if target_y < 0 || target_y >= base_lines.length
|
|
108
|
-
|
|
109
|
-
base_line = base_lines[target_y]
|
|
110
|
-
visible_len = Lipgloss.width(oline)
|
|
111
|
-
|
|
112
|
-
before = base_line[0...x] || ""
|
|
113
|
-
after_start = x + visible_len
|
|
114
|
-
after = base_line[after_start..] || ""
|
|
115
|
-
base_lines[target_y] = before + oline + after
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
base_lines.join("\n")
|
|
119
|
-
end
|
|
120
162
|
end
|
|
121
163
|
end
|
|
122
164
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: claude-office
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Fernando Ruiz
|
|
@@ -90,6 +90,7 @@ extensions: []
|
|
|
90
90
|
extra_rdoc_files: []
|
|
91
91
|
files:
|
|
92
92
|
- LICENSE
|
|
93
|
+
- README.md
|
|
93
94
|
- bin/claude-office
|
|
94
95
|
- lib/claude_office.rb
|
|
95
96
|
- lib/claude_office/agents/agent.rb
|