patchfeld 0.2.0__tar.gz → 0.2.2__tar.gz

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.
Files changed (82) hide show
  1. {patchfeld-0.2.0 → patchfeld-0.2.2}/PKG-INFO +5 -3
  2. {patchfeld-0.2.0 → patchfeld-0.2.2}/README.md +4 -2
  3. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/agents/session.py +31 -1
  4. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/widgets/agent_transcript.py +35 -0
  5. {patchfeld-0.2.0 → patchfeld-0.2.2}/pyproject.toml +1 -1
  6. patchfeld-0.2.2/website/README.md +126 -0
  7. {patchfeld-0.2.0 → patchfeld-0.2.2}/.gitignore +0 -0
  8. {patchfeld-0.2.0 → patchfeld-0.2.2}/LICENSE +0 -0
  9. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/__init__.py +0 -0
  10. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/__main__.py +0 -0
  11. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/actions.py +0 -0
  12. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/activity/__init__.py +0 -0
  13. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/activity/log.py +0 -0
  14. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/agents/__init__.py +0 -0
  15. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/agents/child_tools.py +0 -0
  16. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/agents/fake_sdk_adapter.py +0 -0
  17. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/agents/manager.py +0 -0
  18. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/agents/permission_grants.py +0 -0
  19. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/agents/permission_inbox.py +0 -0
  20. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/agents/request_inbox.py +0 -0
  21. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/agents/sdk_adapter.py +0 -0
  22. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/agents/sort.py +0 -0
  23. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/agents/state.py +0 -0
  24. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/app.py +0 -0
  25. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/config.py +0 -0
  26. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/events.py +0 -0
  27. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/layout/__init__.py +0 -0
  28. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/layout/custom_widgets.py +0 -0
  29. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/layout/defaults.py +0 -0
  30. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/layout/engine.py +0 -0
  31. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/layout/local_widgets.py +0 -0
  32. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/layout/registry.py +0 -0
  33. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/layout/spec.py +0 -0
  34. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/layout/splitter.py +0 -0
  35. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/layout/titles.py +0 -0
  36. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/orchestrator/__init__.py +0 -0
  37. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/orchestrator/formatting.py +0 -0
  38. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/orchestrator/session.py +0 -0
  39. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/orchestrator/tabs_tools.py +0 -0
  40. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/orchestrator/tools.py +0 -0
  41. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/persistence/__init__.py +0 -0
  42. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/persistence/agents_index.py +0 -0
  43. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/persistence/atomic.py +0 -0
  44. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/persistence/layout_store.py +0 -0
  45. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/persistence/layouts_store.py +0 -0
  46. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/persistence/orchestrator_sessions.py +0 -0
  47. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/persistence/paths.py +0 -0
  48. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/persistence/themes_store.py +0 -0
  49. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/persistence/transcript_store.py +0 -0
  50. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/persistence/workspace_store.py +0 -0
  51. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/theme/__init__.py +0 -0
  52. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/theme/engine.py +0 -0
  53. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/theme/spec.py +0 -0
  54. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/widgets/__init__.py +0 -0
  55. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/widgets/_file_lang.py +0 -0
  56. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/widgets/_terminal_keys.py +0 -0
  57. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/widgets/_terminal_render.py +0 -0
  58. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/widgets/activity_feed.py +0 -0
  59. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/widgets/agent_table.py +0 -0
  60. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/widgets/change_cwd_screen.py +0 -0
  61. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/widgets/chrome.py +0 -0
  62. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/widgets/diff_viewer.py +0 -0
  63. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/widgets/file_editor.py +0 -0
  64. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/widgets/file_tree.py +0 -0
  65. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/widgets/file_viewer.py +0 -0
  66. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/widgets/history_screen.py +0 -0
  67. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/widgets/layout_switcher.py +0 -0
  68. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/widgets/log_tail.py +0 -0
  69. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/widgets/markdown.py +0 -0
  70. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/widgets/new_tab_screen.py +0 -0
  71. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/widgets/notebook.py +0 -0
  72. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/widgets/orchestrator_chat.py +0 -0
  73. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/widgets/permission_modal.py +0 -0
  74. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/widgets/permission_request_bar.py +0 -0
  75. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/widgets/resume_screen.py +0 -0
  76. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/widgets/rich_transcript.py +0 -0
  77. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/widgets/system_usage.py +0 -0
  78. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/widgets/terminal.py +0 -0
  79. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/widgets/theme_switcher.py +0 -0
  80. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/widgets/transcript_screen.py +0 -0
  81. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/workspace/__init__.py +0 -0
  82. {patchfeld-0.2.0 → patchfeld-0.2.2}/patchfeld/workspace/spec.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: patchfeld
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: A Textual TUI for managing multiple Claude Code agent sessions
5
5
  Project-URL: Homepage, https://github.com/jimmymills/patchfeld
6
6
  Project-URL: Repository, https://github.com/jimmymills/patchfeld
@@ -39,6 +39,8 @@ Description-Content-Type: text/markdown
39
39
  orchestrator-managed workspace — and lets the agent reshape the UI to fit
40
40
  the work.**
41
41
 
42
+ **[patchfeld.com](https://patchfeld.com)** · [PyPI](https://pypi.org/project/patchfeld/) · [GitHub](https://github.com/jimmymills/patchfeld)
43
+
42
44
  ![patchfeld — orchestrator chat on the left, agent table and activity feed on the right](https://raw.githubusercontent.com/jimmymills/patchfeld/main/docs/images/screenshot.png)
43
45
 
44
46
  ## The pitch
@@ -48,7 +50,7 @@ one's writing tests, one's doing a security pass. They live in three
48
50
  terminal tabs with three scrollbacks, and you're the one mentally juggling
49
51
  which is waiting on what.
50
52
 
51
- **Patchfeld is the room you wish you had.** One TUI. One top-level Claude —
53
+ **Patchfeld is a studio for orchestrating agents.** One TUI. One top-level Claude —
52
54
  the *orchestrator* — runs the show. You tell it what you want done in
53
55
  plain English; it spawns the right children with the right tool
54
56
  allowlists, watches their progress, and pulls them onscreen when they
@@ -118,7 +120,7 @@ the ideas.
118
120
  the actual `claude` CLI, or your shell, in any panel. Mode-C custom
119
121
  widgets let the orchestrator ship Python at runtime when the curated
120
122
  widget library isn't enough.
121
- - **Approve tool calls without leaving the room.** When a child wants
123
+ - **Approve tool calls without leaving the workspace.** When a child wants
122
124
  to use a tool that isn't auto-approved, a modal pops in patchfeld with
123
125
  the tool name and full arguments. Approve once, deny once, always
124
126
  allow this tool for any agent named X (persisted to disk), or always
@@ -4,6 +4,8 @@
4
4
  orchestrator-managed workspace — and lets the agent reshape the UI to fit
5
5
  the work.**
6
6
 
7
+ **[patchfeld.com](https://patchfeld.com)** · [PyPI](https://pypi.org/project/patchfeld/) · [GitHub](https://github.com/jimmymills/patchfeld)
8
+
7
9
  ![patchfeld — orchestrator chat on the left, agent table and activity feed on the right](https://raw.githubusercontent.com/jimmymills/patchfeld/main/docs/images/screenshot.png)
8
10
 
9
11
  ## The pitch
@@ -13,7 +15,7 @@ one's writing tests, one's doing a security pass. They live in three
13
15
  terminal tabs with three scrollbacks, and you're the one mentally juggling
14
16
  which is waiting on what.
15
17
 
16
- **Patchfeld is the room you wish you had.** One TUI. One top-level Claude —
18
+ **Patchfeld is a studio for orchestrating agents.** One TUI. One top-level Claude —
17
19
  the *orchestrator* — runs the show. You tell it what you want done in
18
20
  plain English; it spawns the right children with the right tool
19
21
  allowlists, watches their progress, and pulls them onscreen when they
@@ -83,7 +85,7 @@ the ideas.
83
85
  the actual `claude` CLI, or your shell, in any panel. Mode-C custom
84
86
  widgets let the orchestrator ship Python at runtime when the curated
85
87
  widget library isn't enough.
86
- - **Approve tool calls without leaving the room.** When a child wants
88
+ - **Approve tool calls without leaving the workspace.** When a child wants
87
89
  to use a tool that isn't auto-approved, a modal pops in patchfeld with
88
90
  the tool name and full arguments. Approve once, deny once, always
89
91
  allow this tool for any agent named X (persisted to disk), or always
@@ -50,6 +50,13 @@ class AgentSession:
50
50
  self._send_lock = asyncio.Lock()
51
51
  self._pre_wait_state: AgentState | None = None
52
52
  self._pre_perm_state: AgentState | None = None
53
+ # Tasks created by queue_send() that are still pending or in-flight.
54
+ # Tracked so interrupt() can cancel them before they call
55
+ # adapter.query — otherwise a DirectMessageToAgent queued behind
56
+ # the active stream (e.g. an orchestrator's send_to_agent payload)
57
+ # would wake up the moment the SDK signals end-of-stream and run
58
+ # the queued prompt against the now-interrupted session.
59
+ self._queued_send_tasks: list[asyncio.Task] = []
53
60
 
54
61
  @property
55
62
  def session_id(self) -> str | None:
@@ -79,14 +86,37 @@ class AgentSession:
79
86
  in the same task will correctly block until the send completes —
80
87
  without it, wait_idle could return before the send task acquires the
81
88
  send lock.
89
+
90
+ The task is also tracked so `interrupt()` can cancel it if the user
91
+ interrupts before it has issued its query.
82
92
  """
83
93
  self._idle_event.clear()
84
- return asyncio.create_task(self.send(prompt))
94
+ task = asyncio.create_task(self.send(prompt))
95
+ # Drop completed tasks before appending so the list doesn't grow.
96
+ self._queued_send_tasks = [
97
+ t for t in self._queued_send_tasks if not t.done()
98
+ ]
99
+ self._queued_send_tasks.append(task)
100
+ task.add_done_callback(self._queued_send_tasks.remove)
101
+ return task
85
102
 
86
103
  async def wait_idle(self) -> None:
87
104
  await self._idle_event.wait()
88
105
 
89
106
  async def interrupt(self) -> None:
107
+ # Cancel any send tasks that are still queued (blocked behind the
108
+ # active stream) BEFORE signalling the SDK. Otherwise the SDK's
109
+ # end-of-stream wakes them up and they post their prompt against
110
+ # the now-interrupted session — which is how an orchestrator's
111
+ # `send_to_agent` payload was landing on the child agent after
112
+ # the user pressed ctrl+c.
113
+ pending = [t for t in self._queued_send_tasks if not t.done()]
114
+ for task in pending:
115
+ task.cancel()
116
+ # Wait for cancellations to propagate so the lock is released
117
+ # before the SDK starts winding down the stream.
118
+ if pending:
119
+ await asyncio.gather(*pending, return_exceptions=True)
90
120
  await self._adapter.interrupt()
91
121
 
92
122
  async def stop(self) -> None:
@@ -1,4 +1,5 @@
1
1
  from textual.app import ComposeResult
2
+ from textual.binding import Binding
2
3
  from textual.containers import Vertical
3
4
  from textual.widgets import Input
4
5
 
@@ -23,6 +24,14 @@ class AgentTranscript(Vertical):
23
24
  }
24
25
  """
25
26
 
27
+ # priority=True so the binding fires even when the Input child has
28
+ # focus — without it, ctrl+c is consumed by Textual's default driver
29
+ # handling (which would quit the app). Mirrors OrchestratorChat so
30
+ # both chat panels respond to ctrl+c the same way.
31
+ BINDINGS = [
32
+ Binding("ctrl+c", "interrupt", "interrupt agent", priority=True),
33
+ ]
34
+
26
35
  def __init__(
27
36
  self,
28
37
  *,
@@ -73,6 +82,32 @@ class AgentTranscript(Vertical):
73
82
  bus.publish(DirectMessageToAgent(agent_id=self._agent_id, text=text))
74
83
  event.input.value = ""
75
84
 
85
+ async def action_interrupt(self) -> None:
86
+ manager = getattr(self.app, "manager", None)
87
+ if manager is None:
88
+ return
89
+ # Stale-panel guard: if no live session matches this panel's
90
+ # agent_id, manager.interrupt would silently no-op. Surface that
91
+ # to the user instead — otherwise ctrl+c looks broken when in
92
+ # fact the panel is bound to an agent that no longer exists
93
+ # (e.g. opened from a stale agents.json entry).
94
+ if manager.get_session(self._agent_id) is None:
95
+ try:
96
+ self.app.notify(
97
+ f"no active agent “{self._agent_id}” — panel may be stale",
98
+ severity="warning", timeout=5,
99
+ )
100
+ except Exception:
101
+ pass
102
+ return
103
+ await manager.interrupt(self._agent_id)
104
+ try:
105
+ self.app.notify(
106
+ f"interrupted {self._agent_id}", timeout=3,
107
+ )
108
+ except Exception:
109
+ pass
110
+
76
111
  def rendered_text(self) -> str:
77
112
  """Test helper — delegates to the inner RichTranscript."""
78
113
  return self.query_one(RichTranscript).rendered_text()
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "patchfeld"
3
- version = "0.2.0"
3
+ version = "0.2.2"
4
4
  description = "A Textual TUI for managing multiple Claude Code agent sessions"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -0,0 +1,126 @@
1
+ # patchfeld website
2
+
3
+ Marketing site for [patchfeld](../README.md), hosted at
4
+ [patchfeld.com](https://patchfeld.com).
5
+
6
+ Astro + Tailwind, static-first, single landing page. No backend.
7
+
8
+ ## Stack
9
+
10
+ - [Astro 5](https://astro.build) — static site generator with component islands
11
+ - [Tailwind CSS 4](https://tailwindcss.com) — via the official Vite plugin
12
+ - TypeScript (strict)
13
+ - No animation library — plain CSS + a tiny `IntersectionObserver` for fade-in
14
+
15
+ Astro was chosen because the site is content-first and ships almost zero
16
+ JavaScript by default; the only client-side JS is the copy-to-clipboard
17
+ button and the install-tab switcher (a few dozen lines, inlined per
18
+ component). If we ever need MDX or a docs subsite, Astro accommodates
19
+ both without a rewrite.
20
+
21
+ ## Prerequisites
22
+
23
+ - Node.js 20.x or 22.x (24.x works in dev — official Astro support is 22 LTS)
24
+ - npm 10+
25
+
26
+ ## Local development
27
+
28
+ ```bash
29
+ cd website
30
+ npm install
31
+ npm run dev
32
+ ```
33
+
34
+ The dev server runs on <http://localhost:4321>.
35
+
36
+ ## Build
37
+
38
+ ```bash
39
+ cd website
40
+ npm run build # outputs to website/dist/
41
+ npm run preview # serves the built site
42
+ ```
43
+
44
+ The build is fully static: `dist/` contains HTML, CSS, optimized images,
45
+ and a small bit of JS for the interactive bits.
46
+
47
+ ## Project layout
48
+
49
+ ```
50
+ website/
51
+ package.json
52
+ astro.config.mjs
53
+ tsconfig.json
54
+ src/
55
+ pages/
56
+ index.astro # the whole site is one page for v1
57
+ layouts/
58
+ Base.astro # html shell, fonts, meta, reveal observer
59
+ components/
60
+ Nav.astro
61
+ Hero.astro
62
+ Features.astro
63
+ Screenshot.astro
64
+ Examples.astro
65
+ Widgets.astro
66
+ Install.astro
67
+ Footer.astro
68
+ styles/
69
+ global.css # Tailwind import + design tokens + base
70
+ assets/
71
+ screenshot.png # copied from ../docs/images/screenshot.png
72
+ public/
73
+ favicon.svg
74
+ ```
75
+
76
+ ## Updating the screenshot
77
+
78
+ If the screenshot in the project root (`../docs/images/screenshot.png`)
79
+ is regenerated, copy it back over:
80
+
81
+ ```bash
82
+ cp ../docs/images/screenshot.png src/assets/screenshot.png
83
+ ```
84
+
85
+ `src/assets/` is processed by Astro's image pipeline (compression,
86
+ responsive sizing, lazy loading), which is why we import it via
87
+ `astro:assets` in `Screenshot.astro`.
88
+
89
+ ## Deploying
90
+
91
+ The site is static — any host that serves `dist/` works.
92
+
93
+ ### Cloudflare Pages (recommended)
94
+
95
+ 1. Create a new Pages project pointed at this repo.
96
+ 2. Set the **build command** to `npm run build`.
97
+ 3. Set the **build output directory** to `dist`.
98
+ 4. Set the **root directory** to `website`.
99
+ 5. Add the custom domain `patchfeld.com` once DNS is set up.
100
+
101
+ Cloudflare Pages auto-deploys on push and handles HTTPS for free.
102
+
103
+ ### Alternatives
104
+
105
+ - **Vercel:** import the repo, set the root directory to `website`, framework
106
+ preset "Astro". Vercel auto-detects build command + output.
107
+ - **Netlify:** set base directory `website`, build command `npm run build`,
108
+ publish directory `website/dist`.
109
+ - **GitHub Pages:** publish `dist/` from a workflow. There's no built-in
110
+ Astro action; use [`actions/deploy-pages`](https://github.com/actions/deploy-pages)
111
+ with `actions/upload-pages-artifact` pointed at `website/dist`.
112
+
113
+ ## Updating copy
114
+
115
+ The site's copy is consistent with the project README's voice: direct,
116
+ technical, slightly playful. When you add or rewrite a section, read the
117
+ project root README first and match its register. Don't slip into
118
+ generic SaaS phrasing.
119
+
120
+ GitHub URL: the repo is `jimmymills/patchfeld`. The old `patchbai` URL
121
+ still redirects via GitHub's rename redirect, but everything in the site
122
+ points at the canonical name.
123
+
124
+ ## License
125
+
126
+ MIT — same as the project itself.
File without changes
File without changes
File without changes
File without changes
File without changes