@brimveyn/aimux 1.19.1 → 1.19.3
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.
- package/package.json +1 -1
- package/src/cli/commands/tab/create.ts +45 -5
- package/src/cli/commands/worktree/list.ts +24 -0
- package/src/pty/command-registry.ts +60 -0
- package/src/ui/components/modals/shared/modal-keybinds-overlay.tsx +2 -22
- package/src/ui/components/modals/shared/modal-shell.tsx +2 -35
- package/src/ui/components/overlays/context-menu/context-menu-overlay.tsx +5 -2
- package/src/ui/transparent-fill.ts +49 -0
package/package.json
CHANGED
|
@@ -7,7 +7,11 @@ import {
|
|
|
7
7
|
IPC_CAPABILITY_THIN_ATTACH,
|
|
8
8
|
} from '../../../ipc/protocol'
|
|
9
9
|
import { createPrefixedId } from '../../../platform/id'
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
buildAssistantModelArgs,
|
|
12
|
+
getAllAssistantOptions,
|
|
13
|
+
parseCommand,
|
|
14
|
+
} from '../../../pty/command-registry'
|
|
11
15
|
import { SHARED_FLAGS } from '../../flags'
|
|
12
16
|
import { EXIT_OK, writeJson } from '../../output'
|
|
13
17
|
|
|
@@ -30,6 +34,16 @@ export const tabCreate: CliCommand = {
|
|
|
30
34
|
kind: 'string',
|
|
31
35
|
name: 'command',
|
|
32
36
|
},
|
|
37
|
+
{
|
|
38
|
+
description: 'model for the worker (maps to the assistant’s model flag)',
|
|
39
|
+
kind: 'string',
|
|
40
|
+
name: 'model',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
description: 'reasoning-effort level (maps to the assistant’s effort flag)',
|
|
44
|
+
kind: 'string',
|
|
45
|
+
name: 'effort',
|
|
46
|
+
},
|
|
33
47
|
{
|
|
34
48
|
description: 'worktree id the tab belongs to (defaults to the workspace’s active worktree)',
|
|
35
49
|
kind: 'string',
|
|
@@ -49,8 +63,21 @@ export const tabCreate: CliCommand = {
|
|
|
49
63
|
`unknown assistant: ${assistantId} (known: ${options.map((o) => o.id).join(', ')})`
|
|
50
64
|
)
|
|
51
65
|
}
|
|
52
|
-
const
|
|
53
|
-
typeof ctx.args.flags.command === 'string' ? ctx.args.flags.command :
|
|
66
|
+
const commandOverride =
|
|
67
|
+
typeof ctx.args.flags.command === 'string' ? ctx.args.flags.command : undefined
|
|
68
|
+
const model = typeof ctx.args.flags.model === 'string' ? ctx.args.flags.model : undefined
|
|
69
|
+
const effort = typeof ctx.args.flags.effort === 'string' ? ctx.args.flags.effort : undefined
|
|
70
|
+
|
|
71
|
+
// A full `--command` override owns the whole invocation, so `--model` /
|
|
72
|
+
// `--effort` (which only make sense as additions to the assistant default)
|
|
73
|
+
// would be ambiguous alongside it — reject rather than silently drop them.
|
|
74
|
+
if (commandOverride !== undefined && (model !== undefined || effort !== undefined)) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
'--model / --effort cannot be combined with --command (bake them into --command)'
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const command = commandOverride ?? option.command
|
|
54
81
|
const title = typeof ctx.args.flags.title === 'string' ? ctx.args.flags.title : option.label
|
|
55
82
|
const cwdRaw = typeof ctx.args.flags.cwd === 'string' ? ctx.args.flags.cwd : undefined
|
|
56
83
|
const cwd = cwdRaw === undefined ? undefined : resolvePath(cwdRaw)
|
|
@@ -81,7 +108,11 @@ export const tabCreate: CliCommand = {
|
|
|
81
108
|
// Thin-attach so we don't clobber the UI's dimensions on the same session.
|
|
82
109
|
await daemon.attach({ cols: 0, rows: 0, sessionId: workspace.id, thin: true })
|
|
83
110
|
|
|
84
|
-
const { args, executable } = parseCommand(command)
|
|
111
|
+
const { args: baseArgs, executable } = parseCommand(command)
|
|
112
|
+
// Append the model/effort flags to the assistant default. Throws if the
|
|
113
|
+
// assistant has no control for a requested dimension.
|
|
114
|
+
const modelArgs = buildAssistantModelArgs(option, { effort, model })
|
|
115
|
+
const args = [...baseArgs, ...modelArgs]
|
|
85
116
|
const tabId = createPrefixedId('tab')
|
|
86
117
|
|
|
87
118
|
// cols/rows = 0 means "fall back to the session's last attached size" on
|
|
@@ -101,7 +132,16 @@ export const tabCreate: CliCommand = {
|
|
|
101
132
|
worktreeId,
|
|
102
133
|
})
|
|
103
134
|
|
|
104
|
-
|
|
135
|
+
const resolvedCommand = [executable, ...args].join(' ')
|
|
136
|
+
writeJson({
|
|
137
|
+
assistant: assistantId,
|
|
138
|
+
command: resolvedCommand,
|
|
139
|
+
effort: effort ?? null,
|
|
140
|
+
model: model ?? null,
|
|
141
|
+
tabId,
|
|
142
|
+
title,
|
|
143
|
+
worktreeId,
|
|
144
|
+
})
|
|
105
145
|
return EXIT_OK
|
|
106
146
|
},
|
|
107
147
|
summary: 'Create a new tab in the active workspace',
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { CliCommand } from '../../registry'
|
|
2
2
|
|
|
3
3
|
import { listGitWorktrees } from '../../../git/worktree'
|
|
4
|
+
import { IPC_CAPABILITY_LIST_TABS } from '../../../ipc/protocol'
|
|
4
5
|
import { SHARED_FLAGS } from '../../flags'
|
|
5
6
|
import { EXIT_OK, writeJson } from '../../output'
|
|
6
7
|
|
|
@@ -12,6 +13,28 @@ export const worktreeList: CliCommand = {
|
|
|
12
13
|
const workspace = ctx.getWorkspace()
|
|
13
14
|
const records = workspace.worktrees ?? []
|
|
14
15
|
|
|
16
|
+
// Count live tabs per worktree so an orchestrator can see co-location
|
|
17
|
+
// (several workers sharing one worktree) at a glance. Best-effort: `null`
|
|
18
|
+
// when the daemon is unreachable or predates the listTabs capability —
|
|
19
|
+
// `worktree list` otherwise reads purely from the catalog + git, so we don't
|
|
20
|
+
// want a down daemon to fail it.
|
|
21
|
+
const tabCounts = new Map<string, number>()
|
|
22
|
+
let tabCountsAvailable = false
|
|
23
|
+
try {
|
|
24
|
+
const daemon = await ctx.getDaemon()
|
|
25
|
+
if (daemon.hasCapability(IPC_CAPABILITY_LIST_TABS)) {
|
|
26
|
+
const { tabs } = await daemon.listTabs(workspace.id)
|
|
27
|
+
for (const tab of tabs) {
|
|
28
|
+
if (tab.worktreeId != null) {
|
|
29
|
+
tabCounts.set(tab.worktreeId, (tabCounts.get(tab.worktreeId) ?? 0) + 1)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
tabCountsAvailable = true
|
|
33
|
+
}
|
|
34
|
+
} catch {
|
|
35
|
+
// daemon unreachable — leave tabCount null rather than failing the list.
|
|
36
|
+
}
|
|
37
|
+
|
|
15
38
|
// Cross-check against git so we can flag catalog entries that git no
|
|
16
39
|
// longer knows about (prunable / vanished) — otherwise the CLI would
|
|
17
40
|
// happily report worktrees that don't exist on disk.
|
|
@@ -35,6 +58,7 @@ export const worktreeList: CliCommand = {
|
|
|
35
58
|
path: w.path,
|
|
36
59
|
repoRoot: w.repoRoot,
|
|
37
60
|
source: w.source,
|
|
61
|
+
tabCount: tabCountsAvailable ? (tabCounts.get(w.id) ?? 0) : null,
|
|
38
62
|
})),
|
|
39
63
|
})
|
|
40
64
|
return EXIT_OK
|
|
@@ -1,10 +1,27 @@
|
|
|
1
1
|
import type { AssistantId } from '../state/types'
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* How an assistant renders a vendor-neutral model / reasoning-effort selection
|
|
5
|
+
* into its own CLI args. Vendor flag syntax lives here on the assistant
|
|
6
|
+
* definition — the source of truth — rather than in a separate resolver, so
|
|
7
|
+
* there's exactly one place that knows `claude` takes `--effort` while `codex`
|
|
8
|
+
* takes `-c model_reasoning_effort=…`. An absent builder means the CLI has no
|
|
9
|
+
* control for that dimension, and passing `--model` / `--effort` for it is an
|
|
10
|
+
* error (see `buildAssistantModelArgs`). Values themselves are passed through
|
|
11
|
+
* unvalidated: the worker CLI is the source of truth for its own vocabulary and
|
|
12
|
+
* rejects a bad value with its own clear startup error.
|
|
13
|
+
*/
|
|
14
|
+
export interface AssistantModelSpec {
|
|
15
|
+
buildModelArgs?: (model: string) => string[]
|
|
16
|
+
buildEffortArgs?: (effort: string) => string[]
|
|
17
|
+
}
|
|
18
|
+
|
|
3
19
|
export interface AssistantOption {
|
|
4
20
|
id: AssistantId
|
|
5
21
|
label: string
|
|
6
22
|
command: string
|
|
7
23
|
description: string
|
|
24
|
+
model?: AssistantModelSpec
|
|
8
25
|
}
|
|
9
26
|
|
|
10
27
|
const DEFAULT_SHELL =
|
|
@@ -17,18 +34,30 @@ export const ASSISTANT_OPTIONS: AssistantOption[] = [
|
|
|
17
34
|
description: 'Anthropic Claude CLI',
|
|
18
35
|
id: 'claude',
|
|
19
36
|
label: 'Claude',
|
|
37
|
+
model: {
|
|
38
|
+
buildEffortArgs: (effort) => ['--effort', effort],
|
|
39
|
+
buildModelArgs: (model) => ['--model', model],
|
|
40
|
+
},
|
|
20
41
|
},
|
|
21
42
|
{
|
|
22
43
|
command: 'codex',
|
|
23
44
|
description: 'OpenAI Codex CLI',
|
|
24
45
|
id: 'codex',
|
|
25
46
|
label: 'Codex',
|
|
47
|
+
model: {
|
|
48
|
+
buildEffortArgs: (effort) => ['-c', `model_reasoning_effort=${effort}`],
|
|
49
|
+
buildModelArgs: (model) => ['--model', model],
|
|
50
|
+
},
|
|
26
51
|
},
|
|
27
52
|
{
|
|
28
53
|
command: 'opencode',
|
|
29
54
|
description: 'OpenCode CLI',
|
|
30
55
|
id: 'opencode',
|
|
31
56
|
label: 'OpenCode',
|
|
57
|
+
// OpenCode takes a `provider/model` selector but has no reasoning-effort flag.
|
|
58
|
+
model: {
|
|
59
|
+
buildModelArgs: (model) => ['--model', model],
|
|
60
|
+
},
|
|
32
61
|
},
|
|
33
62
|
{
|
|
34
63
|
command: 'agy',
|
|
@@ -74,3 +103,34 @@ export function parseCommand(commandString: string): { executable: string; args:
|
|
|
74
103
|
const parts = commandString.trim().split(/\s+/).filter(Boolean)
|
|
75
104
|
return { args: parts.slice(1), executable: parts[0] ?? '' }
|
|
76
105
|
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Translate a vendor-neutral `{ model, effort }` selection into the extra CLI
|
|
109
|
+
* args to append to an assistant's default command. Throws if the assistant has
|
|
110
|
+
* no control for a requested dimension (e.g. `--effort` on OpenCode, anything on
|
|
111
|
+
* `terminal`) — that combination is unrepresentable, so we fail at the CLI
|
|
112
|
+
* boundary rather than spawn a worker that silently ignores the request.
|
|
113
|
+
*/
|
|
114
|
+
export function buildAssistantModelArgs(
|
|
115
|
+
option: AssistantOption,
|
|
116
|
+
selection: { model?: string; effort?: string }
|
|
117
|
+
): string[] {
|
|
118
|
+
const args: string[] = []
|
|
119
|
+
const { effort, model } = selection
|
|
120
|
+
|
|
121
|
+
if (model !== undefined && model !== '') {
|
|
122
|
+
if (!option.model?.buildModelArgs) {
|
|
123
|
+
throw new Error(`assistant '${option.id}' does not support --model`)
|
|
124
|
+
}
|
|
125
|
+
args.push(...option.model.buildModelArgs(model))
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (effort !== undefined && effort !== '') {
|
|
129
|
+
if (!option.model?.buildEffortArgs) {
|
|
130
|
+
throw new Error(`assistant '${option.id}' does not support --effort`)
|
|
131
|
+
}
|
|
132
|
+
args.push(...option.model.buildEffortArgs(effort))
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return args
|
|
136
|
+
}
|
|
@@ -1,32 +1,12 @@
|
|
|
1
1
|
import type { ModeId } from '@brimveyn/aimux-config'
|
|
2
2
|
|
|
3
|
-
import { type BoxRenderable, type OptimizedBuffer, RGBA } from '@opentui/core'
|
|
4
|
-
|
|
5
3
|
import { describeBindings } from '../../../../input/keymap/describe-bindings'
|
|
6
4
|
import { useKeymap } from '../../../keymap-context'
|
|
7
5
|
import { useTheme, useTransparent } from '../../../theme'
|
|
6
|
+
import { fillBoxInterior } from '../../../transparent-fill'
|
|
8
7
|
|
|
9
8
|
const KEYS_COLUMN_WIDTH = 12
|
|
10
9
|
|
|
11
|
-
const TRANSPARENT_RGBA = RGBA.fromValues(0, 0, 0, 0)
|
|
12
|
-
|
|
13
|
-
// Same trick as modal-shell: when our bg is transparent, BoxRenderable skips
|
|
14
|
-
// its fill so the chrome chars underneath leak through. setCell bypasses alpha
|
|
15
|
-
// blending and char-preservation, so we can wipe every interior cell to
|
|
16
|
-
// (' ', TRANSPARENT_RGBA, TRANSPARENT_RGBA). No border on this overlay, so
|
|
17
|
-
// fill the full bounds (no 1-cell inset).
|
|
18
|
-
function fillBoxInteriorWithSpaces(this: BoxRenderable, buffer: OptimizedBuffer): void {
|
|
19
|
-
const x0 = this.screenX
|
|
20
|
-
const y0 = this.screenY
|
|
21
|
-
const endX = x0 + this.width
|
|
22
|
-
const endY = y0 + this.height
|
|
23
|
-
for (let y = y0; y < endY; y++) {
|
|
24
|
-
for (let x = x0; x < endX; x++) {
|
|
25
|
-
buffer.setCell(x, y, ' ', TRANSPARENT_RGBA, TRANSPARENT_RGBA)
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
10
|
interface ModalKeybindsOverlayProps {
|
|
31
11
|
modeId: ModeId
|
|
32
12
|
limit?: number
|
|
@@ -52,7 +32,7 @@ export function ModalKeybindsOverlay({ limit, modeId }: ModalKeybindsOverlayProp
|
|
|
52
32
|
paddingRight={2}
|
|
53
33
|
paddingTop={1}
|
|
54
34
|
paddingBottom={1}
|
|
55
|
-
renderAfter={transparent ?
|
|
35
|
+
renderAfter={transparent ? fillBoxInterior : undefined}
|
|
56
36
|
>
|
|
57
37
|
{entries.map((binding) => (
|
|
58
38
|
<box key={binding.description ?? binding.keys} flexDirection="row">
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import type { ModeId } from '@brimveyn/aimux-config'
|
|
2
2
|
import type { ReactNode } from 'react'
|
|
3
3
|
|
|
4
|
-
import { type BoxRenderable, type OptimizedBuffer, RGBA } from '@opentui/core'
|
|
5
|
-
|
|
6
4
|
import { useTheme, useTransparent } from '../../../theme'
|
|
5
|
+
import { fillBorderedBoxInterior } from '../../../transparent-fill'
|
|
7
6
|
import { ModalKeybindsOverlay } from './modal-keybinds-overlay'
|
|
8
7
|
|
|
9
8
|
interface ModalShellProps {
|
|
@@ -16,38 +15,6 @@ interface ModalShellProps {
|
|
|
16
15
|
width: number | `${number}%`
|
|
17
16
|
}
|
|
18
17
|
|
|
19
|
-
const TRANSPARENT_RGBA = RGBA.fromValues(0, 0, 0, 0)
|
|
20
|
-
|
|
21
|
-
// opentui's BoxRenderable only paints a fill when `backgroundColor.a > 0`, so
|
|
22
|
-
// a fully transparent modal bg leaks the chrome characters underneath. The
|
|
23
|
-
// `renderAfter` hook runs after the box paints itself but before its children
|
|
24
|
-
// render, so we manually overwrite every interior cell with a blank space.
|
|
25
|
-
//
|
|
26
|
-
// Two gotchas we hit by reading the zig source (packages/core/src/zig/buffer.zig):
|
|
27
|
-
// 1. `setCellWithAlphaBlending` early-returns via `isFullyTransparent` when
|
|
28
|
-
// both fg and bg alpha are 0 — nothing gets written.
|
|
29
|
-
// 2. Its `blendCells` deliberately preserves the destination char when the
|
|
30
|
-
// overlay char is `DEFAULT_SPACE_CHAR` (codepoint 32) so that drawing a
|
|
31
|
-
// space on top of existing text does not erase it.
|
|
32
|
-
//
|
|
33
|
-
// `setCell` (→ `bufferSetCell` → zig `buffer.set`) bypasses both: it writes
|
|
34
|
-
// the cell unconditionally, no alpha check, no blending, no char preservation.
|
|
35
|
-
function fillModalInteriorWithSpaces(this: BoxRenderable, buffer: OptimizedBuffer): void {
|
|
36
|
-
const x0 = this.screenX
|
|
37
|
-
const y0 = this.screenY
|
|
38
|
-
const w = this.width
|
|
39
|
-
const h = this.height
|
|
40
|
-
const startX = x0 + 1
|
|
41
|
-
const startY = y0 + 1
|
|
42
|
-
const endX = x0 + w - 1
|
|
43
|
-
const endY = y0 + h - 1
|
|
44
|
-
for (let y = startY; y < endY; y++) {
|
|
45
|
-
for (let x = startX; x < endX; x++) {
|
|
46
|
-
buffer.setCell(x, y, ' ', TRANSPARENT_RGBA, TRANSPARENT_RGBA)
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
18
|
export function ModalShell({
|
|
52
19
|
children,
|
|
53
20
|
footer,
|
|
@@ -76,7 +43,7 @@ export function ModalShell({
|
|
|
76
43
|
backgroundColor={bg}
|
|
77
44
|
padding={1}
|
|
78
45
|
width={width}
|
|
79
|
-
renderAfter={transparent ?
|
|
46
|
+
renderAfter={transparent ? fillBorderedBoxInterior : undefined}
|
|
80
47
|
>
|
|
81
48
|
<box width="100%" flexDirection="column" gap={listGap}>
|
|
82
49
|
<box flexDirection="column">
|
|
@@ -9,7 +9,8 @@ import {
|
|
|
9
9
|
type ContextMenuState,
|
|
10
10
|
subscribeContextMenu,
|
|
11
11
|
} from '../../../context-menu/controller'
|
|
12
|
-
import { useTheme } from '../../../theme'
|
|
12
|
+
import { useTheme, useTransparent } from '../../../theme'
|
|
13
|
+
import { fillBorderedBoxInterior } from '../../../transparent-fill'
|
|
13
14
|
|
|
14
15
|
const MenuItem = memo(function MenuItem({
|
|
15
16
|
active,
|
|
@@ -59,6 +60,7 @@ export function ContextMenuOverlay() {
|
|
|
59
60
|
const [menu, setMenu] = useState<ContextMenuState | null>(null)
|
|
60
61
|
const [selected, setSelected] = useState(0)
|
|
61
62
|
const t = useTheme()
|
|
63
|
+
const transparent = useTransparent()
|
|
62
64
|
const terminalCols = useAppStore((s) => s.layout.terminalCols)
|
|
63
65
|
const terminalRows = useAppStore((s) => s.layout.terminalRows)
|
|
64
66
|
|
|
@@ -131,7 +133,8 @@ export function ContextMenuOverlay() {
|
|
|
131
133
|
flexDirection="column"
|
|
132
134
|
border
|
|
133
135
|
borderColor={t.border}
|
|
134
|
-
backgroundColor={t.backgroundPanel}
|
|
136
|
+
backgroundColor={transparent ? 'transparent' : t.backgroundPanel}
|
|
137
|
+
renderAfter={transparent ? fillBorderedBoxInterior : undefined}
|
|
135
138
|
onMouseDown={handleMenuMouseDown}
|
|
136
139
|
>
|
|
137
140
|
{menu.items.map(([label, onSelect], index) => (
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type BoxRenderable, type OptimizedBuffer, RGBA } from '@opentui/core'
|
|
2
|
+
|
|
3
|
+
export const TRANSPARENT_RGBA = RGBA.fromValues(0, 0, 0, 0)
|
|
4
|
+
|
|
5
|
+
// opentui's BoxRenderable only paints a fill when `backgroundColor.a > 0`, so
|
|
6
|
+
// a fully transparent bg leaks the chrome characters underneath. These helpers
|
|
7
|
+
// run from a box's `renderAfter` hook (after the box paints itself, before its
|
|
8
|
+
// children render) and manually overwrite every interior cell with a blank
|
|
9
|
+
// space so the region reads as "emptied" instead of leaking.
|
|
10
|
+
//
|
|
11
|
+
// Two gotchas we hit by reading the zig source (packages/core/src/zig/buffer.zig):
|
|
12
|
+
// 1. `setCellWithAlphaBlending` early-returns via `isFullyTransparent` when
|
|
13
|
+
// both fg and bg alpha are 0 — nothing gets written.
|
|
14
|
+
// 2. Its `blendCells` deliberately preserves the destination char when the
|
|
15
|
+
// overlay char is `DEFAULT_SPACE_CHAR` (codepoint 32) so that drawing a
|
|
16
|
+
// space on top of existing text does not erase it.
|
|
17
|
+
//
|
|
18
|
+
// `setCell` (→ `bufferSetCell` → zig `buffer.set`) bypasses both: it writes
|
|
19
|
+
// the cell unconditionally, no alpha check, no blending, no char preservation.
|
|
20
|
+
|
|
21
|
+
// Bordered boxes: inset by 1 cell so the painted border is preserved.
|
|
22
|
+
export function fillBorderedBoxInterior(this: BoxRenderable, buffer: OptimizedBuffer): void {
|
|
23
|
+
const x0 = this.screenX
|
|
24
|
+
const y0 = this.screenY
|
|
25
|
+
const w = this.width
|
|
26
|
+
const h = this.height
|
|
27
|
+
const startX = x0 + 1
|
|
28
|
+
const startY = y0 + 1
|
|
29
|
+
const endX = x0 + w - 1
|
|
30
|
+
const endY = y0 + h - 1
|
|
31
|
+
for (let y = startY; y < endY; y++) {
|
|
32
|
+
for (let x = startX; x < endX; x++) {
|
|
33
|
+
buffer.setCell(x, y, ' ', TRANSPARENT_RGBA, TRANSPARENT_RGBA)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Borderless boxes: fill the full bounds (no inset).
|
|
39
|
+
export function fillBoxInterior(this: BoxRenderable, buffer: OptimizedBuffer): void {
|
|
40
|
+
const x0 = this.screenX
|
|
41
|
+
const y0 = this.screenY
|
|
42
|
+
const endX = x0 + this.width
|
|
43
|
+
const endY = y0 + this.height
|
|
44
|
+
for (let y = y0; y < endY; y++) {
|
|
45
|
+
for (let x = x0; x < endX; x++) {
|
|
46
|
+
buffer.setCell(x, y, ' ', TRANSPARENT_RGBA, TRANSPARENT_RGBA)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|