@@ -521,6 +521,43 @@ def _pick_key(r: dict) -> tuple:
521521 r ["target" ], r ["path" ])
522522
523523
524+ def _copy_lines (payload : str ) -> list :
525+ """A collapsed copy block nested under a task's bullet.
526+
527+ The page is read on GitHub (often a phone), where the only clipboard
528+ affordance static markdown can offer is the copy button GitHub renders on
529+ fenced code blocks. So every task carries one, holding the exact message
530+ that routes Claude to the task (`/start_dev <prompt-path>`) — tap to
531+ expand, tap to copy, paste into a Claude Code chat. Collapsed behind
532+ `<details>` so each task still reads as one line. The two-space indent
533+ keeps the block inside the bullet's list item; the blank lines around the
534+ fence and after `</details>` are what make GitHub's renderer treat the
535+ fence as markdown and the next bullet as a new list item rather than raw
536+ HTML — do not remove them.
537+ """
538+ return [" <details><summary>📋 copy for Claude</summary>" ,
539+ "" ,
540+ " ```" ,
541+ f" { payload } " ,
542+ " ```" ,
543+ "" ,
544+ " </details>" ]
545+
546+
547+ def _task_item (head : str , payload : str ) -> str :
548+ """One task — the bullet line plus its collapsed copy block."""
549+ return "\n " .join ([head ] + _copy_lines (payload ))
550+
551+
552+ def _items (chunks : list ) -> list :
553+ """Blank-separate multi-line task items so each `</details>` HTML block
554+ ends before the next bullet starts (see `_copy_lines`)."""
555+ out = []
556+ for chunk in chunks :
557+ out += [chunk , "" ]
558+ return out [:- 1 ] if out else []
559+
560+
524561def _bullet (r : dict ) -> str :
525562 """One backlog prompt as a bullet — the phone-readable unit of this page.
526563
@@ -530,7 +567,8 @@ def _bullet(r: dict) -> str:
530567 """
531568 facets = " · " .join (x for x in (r ["target" ], r ["difficulty" ],
532569 r ["autonomy" ], r ["priority" ]) if x != "-" )
533- return f"- [{ _label (r ['title' ])} ]({ r ['path' ]} )" + (f" — { facets } " if facets else "" )
570+ head = f"- [{ _label (r ['title' ])} ]({ r ['path' ]} )" + (f" — { facets } " if facets else "" )
571+ return _task_item (head , f"/start_dev { r ['path' ]} " )
534572
535573
536574def render_dashboard (c : dict ) -> str :
@@ -540,7 +578,9 @@ def render_dashboard(c: dict) -> str:
540578 Heart's dashboard (`/health`). Two rules shape the layout: it must be
541579 *pickable* (the top of the page answers "what should I do now?", not "how
542580 many prompts are there?"), and it must read on a phone (bullets over wide
543- tables, long sections behind `<details>`). Links are repo-root-relative so
581+ tables, long sections behind `<details>`, and a collapsed copy block under
582+ every task so picking one from a phone is copy → paste into a Claude chat,
583+ not retyping a path — see `_copy_lines`). Links are repo-root-relative so
544584 they resolve in GitHub's web and mobile markdown views alike.
545585 """
546586 records = sorted (c ["records" ], key = _pick_key )
@@ -552,7 +592,9 @@ def render_dashboard(c: dict) -> str:
552592 "" ,
553593 "Every task the Mind is holding, on one page: what is in flight, what "
554594 "is parked, and the whole backlog to pick from. Pick a line, then run "
555- "`/start_dev <prompt-path>` to start it." ,
595+ "`/start_dev <prompt-path>` to start it. On a phone, tap **📋 copy "
596+ "for Claude** under a task, copy the block, and paste it into a "
597+ "Claude Code chat to route Claude straight to that task." ,
556598 "" ,
557599 "Tasks only — the organism's health lives with the Heart (`/health`), "
558600 "not here." ,
@@ -581,41 +623,56 @@ def render_dashboard(c: dict) -> str:
581623 shown = rows [:PICK_LIST_MAX ]
582624 more = f" — showing { len (shown )} of { len (rows )} " if len (rows ) > len (shown ) else ""
583625 L += [f"**{ title } ** ({ note } ){ more } " , "" ]
584- L += [_bullet (r ) for r in shown ] or ["- _(none right now)_" ]
626+ L += _items ( [_bullet (r ) for r in shown ]) or ["- _(none right now)_" ]
585627 L += ["" ]
586628
587629 L += ["## In flight" , "" ,
588630 "Issued — each has an open GitHub issue and usually a branch. The "
589631 "full record for each is in [`active.md`](active.md)." , "" ]
632+ flight = []
590633 for r in c ["in_flight" ]:
591634 issue = f" — [issue #{ r ['issue_no' ]} ]({ r ['issue' ]} )" if r ["issue_no" ] else ""
592635 status = f" — { _clip (r ['status' ])} " if r ["status" ] else ""
593- L .append (f"- [{ _label (r ['title' ])} ]({ r ['path' ]} ){ issue } { status } " )
594- L += ([] if c ["in_flight" ] else ["- _(nothing in flight)_" ]) + ["" ]
595-
596- for key , heading , blurb in (
597- ("parked" , "Parked" , "Started or scoped, not currently in flight — "
598- "resume by moving the row back to `active.md`. "
599- "Full detail in [`parked.md`](parked.md)." ),
600- ("planned" , "Planned" , "Scoped but not started; some are not yet prompt "
601- "files. Full detail in [`planned.md`](planned.md)." ),
636+ flight .append (_task_item (
637+ f"- [{ _label (r ['title' ])} ]({ r ['path' ]} ){ issue } { status } " ,
638+ f"/start_dev { r ['path' ]} " ))
639+ L += _items (flight ) or ["- _(nothing in flight)_" ]
640+ L += ["" ]
641+
642+ for key , heading , verb , blurb in (
643+ ("parked" , "Parked" , "resume" ,
644+ "Started or scoped, not currently in flight — "
645+ "resume by moving the row back to `active.md`. "
646+ "Full detail in [`parked.md`](parked.md)." ),
647+ ("planned" , "Planned" , "start" ,
648+ "Scoped but not started; some are not yet prompt "
649+ "files. Full detail in [`planned.md`](planned.md)." ),
602650 ):
603651 rows = c [key ]
604652 L += [f"## { heading } " , "" , blurb , "" ,
605653 "<details>" , f"<summary><b>{ len (rows )} </b> task(s)</summary>" , "" ]
654+ items = []
606655 for e in rows :
607656 issue = f" — [issue #{ e ['issue_no' ]} ]({ e ['issue' ]} )" if e ["issue_no" ] else ""
608657 status = f" — { _clip (e ['status' ])} " if e ["status" ] else ""
609- L .append (f"- **{ _label (e ['slug' ])} **{ issue } { status } " )
610- L += ([] if rows else ["- _(none)_" ]) + ["" , "</details>" , "" ]
658+ # A registry row may name its prompt file; without one there is no
659+ # start_dev target, so route the slug as free prose instead.
660+ prompt = (e ["prompt" ].split () or ["" ])[0 ]
661+ payload = (f"/start_dev { prompt } " if prompt .endswith (".md" ) else
662+ f"/route { verb } the { key } PyAutoMind task "
663+ f"{ e ['slug' ]} — its record is in { key } .md" )
664+ items .append (_task_item (
665+ f"- **{ _label (e ['slug' ])} **{ issue } { status } " , payload ))
666+ L += _items (items ) or ["- _(none)_" ]
667+ L += ["" , "</details>" , "" ]
611668
612669 L += [f"## Backlog" , "" ,
613670 f"**{ c ['total' ]} ** filed prompts, not started. Each section is sorted "
614671 "most-pickable first (priority, then size)." , "" ]
615672 for wt , n in c ["by_work_type" ].items ():
616673 rows = [r for r in records if r ["work_type" ] == wt ]
617674 L += ["<details>" , f"<summary><b>{ wt } </b> — { n } </summary>" , "" ]
618- L += [_bullet (r ) for r in rows ]
675+ L += _items ( [_bullet (r ) for r in rows ])
619676 L += ["" , "</details>" , "" ]
620677
621678 if c ["hygiene" ]:
0 commit comments