design multitab pane widget (T-83)
Reusable widget for panes that need N runtime tab instances of the same kind. First consumer is the Claude pane (primary + 0..N secondaries per D-41); the generic shape lets other panes adopt it later without reinventing tab strips. Includes: - Design doc with API sketch, rendering, interaction, persistence boundary, integration sketch for the Claude pane, and three open questions (keyboard scoping for nested cases, overflow, density). - Wireframe of the Claude pane with primary (pinned) + 2 secondaries + add button, accent border on the active tab. - Architecture diagram (sketch mode) showing the host / widget / controller / IPC boundary that keeps the widget domain-free. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
title: MultitabPane — architecture {
|
||||
near: top-center
|
||||
shape: text
|
||||
style.font-size: 24
|
||||
style.bold: true
|
||||
}
|
||||
|
||||
direction: down
|
||||
|
||||
host: ClaudePane (host) {
|
||||
shape: rectangle
|
||||
style.fill: "#13161c"
|
||||
style.stroke: "#7c5cff"
|
||||
style.font-color: "#e8ecf2"
|
||||
|
||||
state: ChangeNotifier — owns lifecycle {
|
||||
shape: rectangle
|
||||
style.fill: "#0e1014"
|
||||
style.stroke: "#262a32"
|
||||
style.font-color: "#a0a8b8"
|
||||
}
|
||||
}
|
||||
|
||||
widget: MultitabPane<T> (widget) {
|
||||
shape: rectangle
|
||||
style.fill: "#13161c"
|
||||
style.stroke: "#262a32"
|
||||
style.font-color: "#e8ecf2"
|
||||
|
||||
shell: builds tabstrip + body shell {
|
||||
shape: rectangle
|
||||
style.fill: "#0e1014"
|
||||
style.stroke: "#262a32"
|
||||
style.font-color: "#a0a8b8"
|
||||
}
|
||||
}
|
||||
|
||||
controller: MultitabController<T> {
|
||||
shape: rectangle
|
||||
style.fill: "#13161c"
|
||||
style.stroke: "#262a32"
|
||||
style.font-color: "#e8ecf2"
|
||||
|
||||
api: |md
|
||||
add(entry)
|
||||
remove(id)
|
||||
activate(id)
|
||||
reorder(id, idx)
|
||||
| {
|
||||
style.font-color: "#a0a8b8"
|
||||
}
|
||||
}
|
||||
|
||||
entries: List<MultitabEntry<T>> {
|
||||
shape: rectangle
|
||||
style.fill: "#0e1014"
|
||||
style.stroke: "#262a32"
|
||||
style.font-color: "#a0a8b8"
|
||||
|
||||
primary: primary {
|
||||
shape: rectangle
|
||||
style.fill: "#1a1f28"
|
||||
style.stroke: "#7c5cff"
|
||||
style.font-color: "#e8ecf2"
|
||||
closeable\: false: { shape: text; style.font-color: "#7a8294"; style.font-size: 10 }
|
||||
reorderable\: false: { shape: text; style.font-color: "#7a8294"; style.font-size: 10 }
|
||||
}
|
||||
sec1: secondary 1 {
|
||||
shape: rectangle
|
||||
style.fill: "#13161c"
|
||||
style.stroke: "#262a32"
|
||||
style.font-color: "#a0a8b8"
|
||||
}
|
||||
sec2: secondary 2 {
|
||||
shape: rectangle
|
||||
style.fill: "#13161c"
|
||||
style.stroke: "#262a32"
|
||||
style.font-color: "#a0a8b8"
|
||||
}
|
||||
}
|
||||
|
||||
ipc: kernel.claude / IPC {
|
||||
shape: rectangle
|
||||
style.fill: "#13161c"
|
||||
style.stroke: "#5a8c5a"
|
||||
style.font-color: "#e8ecf2"
|
||||
|
||||
spawn: spawnSecondary() {
|
||||
shape: rectangle
|
||||
style.fill: "#0e1014"
|
||||
style.stroke: "#262a32"
|
||||
style.font-color: "#a0a8b8"
|
||||
}
|
||||
close: closeSecondary(ref) {
|
||||
shape: rectangle
|
||||
style.fill: "#0e1014"
|
||||
style.stroke: "#262a32"
|
||||
style.font-color: "#a0a8b8"
|
||||
}
|
||||
}
|
||||
|
||||
host -> controller: owns {
|
||||
style.stroke: "#7a8294"
|
||||
style.font-color: "#7a8294"
|
||||
}
|
||||
host -> widget: builds with {
|
||||
style.stroke: "#7a8294"
|
||||
style.font-color: "#7a8294"
|
||||
}
|
||||
controller -> entries: holds {
|
||||
style.stroke: "#7a8294"
|
||||
style.font-color: "#7a8294"
|
||||
}
|
||||
widget -> controller: subscribes (Listenable) {
|
||||
style.stroke: "#7c5cff"
|
||||
style.font-color: "#a0a8b8"
|
||||
}
|
||||
|
||||
widget -> host: onAddRequested() {
|
||||
style.stroke: "#d97757"
|
||||
style.font-color: "#d97757"
|
||||
}
|
||||
widget -> host: onCloseRequested(entry) {
|
||||
style.stroke: "#d97757"
|
||||
style.font-color: "#d97757"
|
||||
}
|
||||
|
||||
host -> ipc: routes user intent {
|
||||
style.stroke: "#5a8c5a"
|
||||
style.font-color: "#5a8c5a"
|
||||
}
|
||||
ipc.spawn -> entries.sec2: appends new entry {
|
||||
style.stroke: "#5a8c5a"
|
||||
style.font-color: "#5a8c5a"
|
||||
}
|
||||
ipc.close -> entries.sec1: removes entry {
|
||||
style.stroke: "#d97757"
|
||||
style.font-color: "#d97757"
|
||||
}
|
||||
|
||||
note: |md
|
||||
### Boundary
|
||||
|
||||
**Widget** is generic. It knows
|
||||
about `MultitabEntry<T>` and routes
|
||||
user gestures back to the host. It
|
||||
never touches PTY, IPC, or session
|
||||
naming.
|
||||
|
||||
**Host** owns the controller and
|
||||
decides what `add` / `close` mean
|
||||
in the domain — for Claude that's
|
||||
spawning/killing tmux sessions
|
||||
via IPC.
|
||||
|
||||
This boundary is what makes the
|
||||
widget reusable: any pane that
|
||||
needs N runtime instances can
|
||||
drop it in with their own host
|
||||
and payload type.
|
||||
| {
|
||||
shape: rectangle
|
||||
style.fill: "#13161c"
|
||||
style.stroke: "#262a32"
|
||||
style.font-color: "#a0a8b8"
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1009 KiB |
Reference in New Issue
Block a user