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:
+32
-4
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"exported_at": "2026-05-06T09:14:06Z",
|
||||
"exported_at": "2026-05-06T10:04:42Z",
|
||||
"decisions": [
|
||||
{
|
||||
"id": "D-1",
|
||||
@@ -2040,11 +2040,11 @@
|
||||
"type": "task",
|
||||
"parent_id": "T-4",
|
||||
"title": "implement welcome screen per hi-fi design",
|
||||
"status": "ready",
|
||||
"status": "done",
|
||||
"priority": "medium",
|
||||
"decision_ref": "D-43",
|
||||
"created_at": "2026-04-22 14:08:40",
|
||||
"updated_at": "2026-05-03 20:34:52"
|
||||
"updated_at": "2026-05-06 09:17:22"
|
||||
},
|
||||
{
|
||||
"id": "T-22",
|
||||
@@ -2673,9 +2673,23 @@
|
||||
"priority": "low",
|
||||
"created_at": "2026-05-06 08:30:15",
|
||||
"updated_at": "2026-05-06 08:30:15"
|
||||
},
|
||||
{
|
||||
"id": "T-83",
|
||||
"type": "task",
|
||||
"title": "design reusable sortable tab system for multitab panes",
|
||||
"status": "in_progress",
|
||||
"priority": "high",
|
||||
"created_at": "2026-05-06 09:17:39",
|
||||
"updated_at": "2026-05-06 09:17:50"
|
||||
}
|
||||
],
|
||||
"ticket_deps": [
|
||||
{
|
||||
"blocker_id": "T-83",
|
||||
"blocked_id": "T-24"
|
||||
}
|
||||
],
|
||||
"ticket_deps": null,
|
||||
"ticket_labels": null,
|
||||
"history": [
|
||||
{
|
||||
@@ -4005,6 +4019,20 @@
|
||||
"old_value": "in_progress",
|
||||
"new_value": "done",
|
||||
"changed_at": "2026-05-06 07:56:10"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-21",
|
||||
"field": "status",
|
||||
"old_value": "ready",
|
||||
"new_value": "done",
|
||||
"changed_at": "2026-05-06 09:17:22"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-83",
|
||||
"field": "status",
|
||||
"old_value": "backlog",
|
||||
"new_value": "in_progress",
|
||||
"changed_at": "2026-05-06 09:17:50"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
# Multitab pane — design
|
||||
|
||||
Ticket: T-83
|
||||
Drives: T-24 (secondary Claude pane UI wiring)
|
||||
Date: 2026-05-06
|
||||
|
||||
## Problem
|
||||
|
||||
Some panes need to host multiple, dynamically-spawned views of the
|
||||
same kind. The first concrete case is the Claude pane: per
|
||||
[D-41](../../decisions/architecture.md#d-41-claude-panes-one-primary-per-repo-tmux-backed),
|
||||
each repo has exactly one **primary** Claude pane plus zero or more
|
||||
**secondary** panes spawned at runtime. The user needs a way to:
|
||||
|
||||
- See which Claude sessions are open
|
||||
- Switch between them
|
||||
- Spawn a new secondary
|
||||
- Close a secondary (primary has no close affordance)
|
||||
|
||||
The kernel's existing `TabContribution` system addresses a different
|
||||
need — it lets extensions statically declare which widget shows up in
|
||||
which **panel slot** (sidebar, workspace, context). It does not
|
||||
support dynamic tab instances *within* a single contribution.
|
||||
|
||||
This design fills that gap with a reusable widget, so future panes
|
||||
that need the same shape (potentially the editor — see D-48 — or
|
||||
diff/preview surfaces) can adopt it without reinventing tab strips.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Replacing `TabContribution`. Slot-host tabs are static-by-design;
|
||||
this widget is for inside-a-tab dynamism.
|
||||
- Window-level tab management (browser-style "tear off into a window").
|
||||
- Editor multi-buffer tabs. [D-48](../../decisions/architecture.md#d-48-chrome-budget-no-tabs-no-breadcrumbs-keyboard-first)
|
||||
rejected those; revisiting is a separate decision.
|
||||
|
||||
## API sketch
|
||||
|
||||
```dart
|
||||
class MultitabPane<T> extends StatefulWidget {
|
||||
const MultitabPane({
|
||||
required this.controller,
|
||||
required this.tabBuilder,
|
||||
required this.bodyBuilder,
|
||||
this.onCloseRequested,
|
||||
this.onAddRequested,
|
||||
this.allowReorder = true,
|
||||
});
|
||||
|
||||
final MultitabController<T> controller;
|
||||
final Widget Function(BuildContext, MultitabEntry<T>) tabBuilder;
|
||||
final Widget Function(BuildContext, MultitabEntry<T>) bodyBuilder;
|
||||
final void Function(MultitabEntry<T> entry)? onCloseRequested;
|
||||
final void Function()? onAddRequested;
|
||||
final bool allowReorder;
|
||||
}
|
||||
|
||||
class MultitabEntry<T> {
|
||||
final String id; // stable identity (e.g. "claude.primary")
|
||||
final String title; // display label
|
||||
final bool closeable; // primary tabs set this false
|
||||
final bool reorderable; // primary often pinned to position 0
|
||||
final T payload; // domain object the bodyBuilder renders
|
||||
}
|
||||
|
||||
class MultitabController<T> extends ChangeNotifier {
|
||||
List<MultitabEntry<T>> get entries;
|
||||
MultitabEntry<T>? get active;
|
||||
|
||||
void add(MultitabEntry<T> entry, {bool activate = true});
|
||||
void remove(String id);
|
||||
void activate(String id);
|
||||
void reorder(String id, int newIndex);
|
||||
}
|
||||
```
|
||||
|
||||
The widget is a thin shell:
|
||||
- Renders the tab strip via `ClideTabBar` (or a reorderable variant)
|
||||
- Calls `bodyBuilder(active)` for the visible content
|
||||
- Routes user gestures to controller methods or callbacks
|
||||
- Emits `onCloseRequested` / `onAddRequested` so the host decides
|
||||
the actual lifecycle (e.g. Claude pane spawns a new tmux session,
|
||||
doesn't just append a UI tab)
|
||||
|
||||
The host owns the controller and the payload type. The widget never
|
||||
touches PTY, IPC, or Claude session naming.
|
||||
|
||||
## Rendering
|
||||
|
||||
The tab strip lives at the top of the pane chrome. Layout:
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────┐
|
||||
│ [primary] [secondary 1] [secondary 2] [+] │
|
||||
├──────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ active tab body │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
- Active tab: filled background, bright text
|
||||
- Inactive: muted background, muted text
|
||||
- Close glyph (×) appears on hover for `closeable` tabs
|
||||
- `+` button at the end if `onAddRequested` is set
|
||||
- Drag-to-reorder respects `reorderable`; non-reorderable tabs
|
||||
(primary) are pinned to position 0 and other tabs cannot be
|
||||
dropped before them
|
||||
|
||||
## Interaction
|
||||
|
||||
- **Click a tab** → activate
|
||||
- **Click ×** → call `onCloseRequested(entry)`; host decides whether
|
||||
to confirm, kill the underlying session, etc.
|
||||
- **Drag-and-drop** → call `controller.reorder(id, newIndex)` after
|
||||
the gesture completes; controller enforces pinned positions
|
||||
- **Click +** → call `onAddRequested()`; host creates the new entry
|
||||
and adds it via `controller.add(...)`
|
||||
- **Keyboard**: `⌘1`–`⌘9` jump to tab N; `⌘W` close active (skipped
|
||||
for non-closeable); `⌘⇧[` / `⌘⇧]` cycle prev/next
|
||||
|
||||
## Persistence
|
||||
|
||||
Out of scope for the widget. Hosts that want to persist tab order or
|
||||
which tabs were open across sessions read/write through their own
|
||||
settings layer and seed the controller on init.
|
||||
|
||||
## Claude pane integration (T-24)
|
||||
|
||||
```
|
||||
ClaudePane (host)
|
||||
└── MultitabPane<ClaudeSessionRef>(
|
||||
controller: claudeTabsController,
|
||||
tabBuilder: (ctx, e) => Text(e.title),
|
||||
bodyBuilder: (ctx, e) => ClaudePaneBody(session: e.payload),
|
||||
onAddRequested: () => kernel.claude.spawnSecondary(),
|
||||
onCloseRequested: (e) => kernel.claude.closeSecondary(e.payload),
|
||||
)
|
||||
```
|
||||
|
||||
`ClaudeSessionRef` carries the tmux session name + isPrimary. The
|
||||
controller is seeded with `[primary]` on boot; secondaries get
|
||||
appended as the user clicks `+`. Closing a secondary triggers
|
||||
`pane.close` IPC and removes the entry; closing the primary is not
|
||||
exposed (`closeable: false`).
|
||||
|
||||
## What ships in this ticket
|
||||
|
||||
T-83 delivers:
|
||||
1. `MultitabPane` widget + `MultitabController` + `MultitabEntry`
|
||||
under `lib/widgets/src/`
|
||||
2. Unit tests for controller invariants (pinned positions, active
|
||||
selection survives close, reorder bounds)
|
||||
3. Widget tests for the strip (selection, close hover, add button,
|
||||
drag-reorder)
|
||||
4. This design doc
|
||||
|
||||
T-24 picks up after and wires the Claude pane to it.
|
||||
|
||||
## Open questions
|
||||
|
||||
- **Q: Where does keyboard handling live?** Host or widget?
|
||||
Recommendation: widget owns `⌘W` / `⌘1`–`⌘9` / cycle; host wires
|
||||
them via the existing kernel commands surface. Avoids each host
|
||||
reinventing the same shortcuts.
|
||||
|
||||
**Nesting caveat:** the widget composes (a Claude tab can host
|
||||
its own `MultitabPane<EditorBuffer>` etc.). Shortcut handling
|
||||
must be scoped to the focus subtree, not registered globally —
|
||||
otherwise the outermost pane consumes `⌘W` even when the user
|
||||
is typing in a nested tab. Implementation: wrap shortcuts in a
|
||||
`Shortcuts` / `Actions` widget inside the pane's `Focus` scope
|
||||
so the innermost focused pane wins via Flutter's normal
|
||||
shortcut-resolution chain.
|
||||
|
||||
- **Q: Tab overflow** when many secondaries open? Recommendation:
|
||||
start with horizontal scroll; revisit if it becomes a problem.
|
||||
|
||||
- **Q: Tab-strip visual style** — match `ClideTabBar` exactly, or
|
||||
introduce a denser variant for inside-pane use? Recommendation:
|
||||
reuse `ClideTabBar` initially; spin off a `ClideTabBar.dense`
|
||||
variant only if visual hierarchy issues emerge.
|
||||
@@ -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 |
@@ -0,0 +1,209 @@
|
||||
{
|
||||
"name": "Claude Pane — multitab",
|
||||
"shapes": {
|
||||
"canvas": {
|
||||
"type": "Rectangle",
|
||||
"left": 40, "top": 40, "width": 1100, "height": 720,
|
||||
"fillColor": "#0e1014",
|
||||
"strokeColor": "#1c2028"
|
||||
},
|
||||
|
||||
"pane-header": {
|
||||
"type": "Rectangle",
|
||||
"parent": "canvas",
|
||||
"left": 40, "top": 40, "width": 1100, "height": 36,
|
||||
"fillColor": "#13161c",
|
||||
"strokeColor": "#1c2028"
|
||||
},
|
||||
"pane-title": {
|
||||
"type": "Text",
|
||||
"parent": "pane-header",
|
||||
"left": 56, "top": 50,
|
||||
"text": "claude — secondary 2",
|
||||
"fontColor": "#e8ecf2",
|
||||
"fontSize": 12
|
||||
},
|
||||
"pane-subtitle": {
|
||||
"type": "Text",
|
||||
"parent": "pane-header",
|
||||
"left": 56, "top": 64,
|
||||
"text": "tmux · clide-claude-var-mnt-data-projects-clide-2",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 10
|
||||
},
|
||||
|
||||
"tabstrip": {
|
||||
"type": "Rectangle",
|
||||
"parent": "canvas",
|
||||
"left": 40, "top": 76, "width": 1100, "height": 32,
|
||||
"fillColor": "#0e1014",
|
||||
"strokeColor": "#1c2028"
|
||||
},
|
||||
|
||||
"tab-primary": {
|
||||
"type": "Rectangle",
|
||||
"parent": "tabstrip",
|
||||
"left": 56, "top": 80, "width": 132, "height": 28,
|
||||
"fillColor": "#13161c",
|
||||
"strokeColor": "#1c2028",
|
||||
"corners": [4, 4, 0, 0]
|
||||
},
|
||||
"tab-primary-pin": {
|
||||
"type": "Text",
|
||||
"parent": "tab-primary",
|
||||
"left": 64, "top": 86,
|
||||
"text": "📌",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 10
|
||||
},
|
||||
"tab-primary-text": {
|
||||
"type": "Text",
|
||||
"parent": "tab-primary",
|
||||
"left": 84, "top": 86,
|
||||
"text": "primary",
|
||||
"fontColor": "#a0a8b8",
|
||||
"fontSize": 12
|
||||
},
|
||||
|
||||
"tab-sec-1": {
|
||||
"type": "Rectangle",
|
||||
"parent": "tabstrip",
|
||||
"left": 192, "top": 80, "width": 132, "height": 28,
|
||||
"fillColor": "#13161c",
|
||||
"strokeColor": "#1c2028",
|
||||
"corners": [4, 4, 0, 0]
|
||||
},
|
||||
"tab-sec-1-text": {
|
||||
"type": "Text",
|
||||
"parent": "tab-sec-1",
|
||||
"left": 204, "top": 86,
|
||||
"text": "secondary 1",
|
||||
"fontColor": "#a0a8b8",
|
||||
"fontSize": 12
|
||||
},
|
||||
"tab-sec-1-close": {
|
||||
"type": "Text",
|
||||
"parent": "tab-sec-1",
|
||||
"left": 304, "top": 86,
|
||||
"text": "×",
|
||||
"fontColor": "#5a6478",
|
||||
"fontSize": 14
|
||||
},
|
||||
|
||||
"tab-sec-2": {
|
||||
"type": "Rectangle",
|
||||
"parent": "tabstrip",
|
||||
"left": 328, "top": 80, "width": 132, "height": 28,
|
||||
"fillColor": "#1a1f28",
|
||||
"strokeColor": "#7c5cff",
|
||||
"corners": [4, 4, 0, 0]
|
||||
},
|
||||
"tab-sec-2-text": {
|
||||
"type": "Text",
|
||||
"parent": "tab-sec-2",
|
||||
"left": 340, "top": 86,
|
||||
"text": "secondary 2",
|
||||
"fontColor": "#e8ecf2",
|
||||
"fontSize": 12
|
||||
},
|
||||
"tab-sec-2-close": {
|
||||
"type": "Text",
|
||||
"parent": "tab-sec-2",
|
||||
"left": 440, "top": 86,
|
||||
"text": "×",
|
||||
"fontColor": "#a0a8b8",
|
||||
"fontSize": 14
|
||||
},
|
||||
|
||||
"tab-add": {
|
||||
"type": "Rectangle",
|
||||
"parent": "tabstrip",
|
||||
"left": 464, "top": 80, "width": 28, "height": 28,
|
||||
"fillColor": "#0e1014",
|
||||
"strokeColor": "#1c2028",
|
||||
"corners": [4, 4, 0, 0]
|
||||
},
|
||||
"tab-add-glyph": {
|
||||
"type": "Text",
|
||||
"parent": "tab-add",
|
||||
"left": 474, "top": 86,
|
||||
"text": "+",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 14
|
||||
},
|
||||
|
||||
"active-divider": {
|
||||
"type": "Rectangle",
|
||||
"parent": "canvas",
|
||||
"left": 40, "top": 108, "width": 1100, "height": 2,
|
||||
"fillColor": "#7c5cff",
|
||||
"strokeColor": "#7c5cff"
|
||||
},
|
||||
|
||||
"body": {
|
||||
"type": "Rectangle",
|
||||
"parent": "canvas",
|
||||
"left": 40, "top": 110, "width": 1100, "height": 650,
|
||||
"fillColor": "#0e1014",
|
||||
"strokeColor": "#1c2028"
|
||||
},
|
||||
"body-banner-name": {
|
||||
"type": "Text",
|
||||
"parent": "body",
|
||||
"left": 56, "top": 132,
|
||||
"text": "Claude Code v2.1.128",
|
||||
"fontColor": "#e8ecf2",
|
||||
"fontSize": 12
|
||||
},
|
||||
"body-banner-meta": {
|
||||
"type": "Text",
|
||||
"parent": "body",
|
||||
"left": 56, "top": 148,
|
||||
"text": "Opus 4.7 (1M context) · fresh secondary session",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 11
|
||||
},
|
||||
|
||||
"body-msg-prompt": {
|
||||
"type": "Text",
|
||||
"parent": "body",
|
||||
"left": 56, "top": 200,
|
||||
"text": "› dig into the failing test in test/pty/session_test.dart",
|
||||
"fontColor": "#a0a8b8",
|
||||
"fontSize": 12
|
||||
},
|
||||
"body-msg-resp": {
|
||||
"type": "Text",
|
||||
"parent": "body",
|
||||
"left": 56, "top": 226,
|
||||
"text": "● Looking at the write-keystrokes test. The shell process\n starts but the echo doesn't appear in the output stream.\n Let me trace the write path…",
|
||||
"fontColor": "#e8ecf2",
|
||||
"fontSize": 12
|
||||
},
|
||||
|
||||
"prompt-divider": {
|
||||
"type": "Rectangle",
|
||||
"parent": "body",
|
||||
"left": 40, "top": 700, "width": 1100, "height": 1,
|
||||
"fillColor": "#1c2028",
|
||||
"strokeColor": "#1c2028"
|
||||
},
|
||||
"prompt": {
|
||||
"type": "Text",
|
||||
"parent": "body",
|
||||
"left": 56, "top": 712,
|
||||
"text": "› Try \"run the test in this pane\"",
|
||||
"fontColor": "#5a6478",
|
||||
"fontSize": 12
|
||||
},
|
||||
|
||||
"anno": {
|
||||
"type": "Text",
|
||||
"parent": "canvas",
|
||||
"left": 720, "top": 80,
|
||||
"text": "active tab gets accent border + bottom rule",
|
||||
"fontColor": "#5a6478",
|
||||
"fontSize": 10
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 61 KiB |
Reference in New Issue
Block a user