refactor: remove unused V2 code (#39699)
Co-authored-by: Aiden Cline <rekram1-node@users.noreply.github.com>
This commit is contained in:
parent
bd132f2614
commit
0ffec67ca3
11 changed files with 0 additions and 1114 deletions
|
|
@ -11,8 +11,6 @@ import { Permission } from "../permission"
|
||||||
// Combined output files written by the Shell service, e.g. `<data>/shell/<projectID>/<shellID>.out`.
|
// Combined output files written by the Shell service, e.g. `<data>/shell/<projectID>/<shellID>.out`.
|
||||||
// Whitelisted so agents can read a command's full captured output without an external-directory prompt.
|
// Whitelisted so agents can read a command's full captured output without an external-directory prompt.
|
||||||
const SHELL_OUTPUT_GLOB = path.join(Global.Path.data, "shell", "*", "*")
|
const SHELL_OUTPUT_GLOB = path.join(Global.Path.data, "shell", "*", "*")
|
||||||
const BUILD_SYSTEM =
|
|
||||||
"You are an AI coding agent. Help the user accomplish software engineering tasks by inspecting the workspace, making targeted changes, and using tools according to the configured permissions."
|
|
||||||
|
|
||||||
const PROMPT_EXPLORE = `You are a file search specialist. You excel at thoroughly navigating and exploring codebases.
|
const PROMPT_EXPLORE = `You are a file search specialist. You excel at thoroughly navigating and exploring codebases.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,436 +0,0 @@
|
||||||
import { OptimizedBuffer, RGBA, TextAttributes } from "@opentui/core"
|
|
||||||
import { go } from "../logo"
|
|
||||||
|
|
||||||
const PERIOD = 4600
|
|
||||||
const RINGS = 3
|
|
||||||
const WIDTH = 3.8
|
|
||||||
const TAIL = 9.5
|
|
||||||
const AMP = 0.55
|
|
||||||
const TAIL_AMP = 0.16
|
|
||||||
const BREATH_AMP = 0.05
|
|
||||||
const BREATH_SPEED = 0.0008
|
|
||||||
// Offset so the bg ring emits from the estimated GO center when the logo shimmer peaks.
|
|
||||||
const PHASE_OFFSET = 0.29
|
|
||||||
const LOGO_GAP = 1
|
|
||||||
const LOGO_TOP_BIAS = -1
|
|
||||||
const LOGO_LEFT_WIDTH = go.left[0]?.length ?? 0
|
|
||||||
const LOGO_LINES = go.left.map((line, index) => line + " ".repeat(LOGO_GAP) + go.right[index])
|
|
||||||
const LOGO_WIDTH = LOGO_LINES[0]?.length ?? 0
|
|
||||||
const LOGO_HEIGHT = LOGO_LINES.length
|
|
||||||
const SPACE = " ".codePointAt(0)!
|
|
||||||
const TOP_HALF = "▀".codePointAt(0)!
|
|
||||||
const FULL_BLOCK = "█".codePointAt(0)!
|
|
||||||
const RING_SCALE = 1 / RINGS
|
|
||||||
const TAIL_SCALE = 1 / TAIL
|
|
||||||
const LOGO_REACH = Math.hypot(LOGO_WIDTH, LOGO_HEIGHT * 2) + 3
|
|
||||||
|
|
||||||
const enum LogoCellKind {
|
|
||||||
Background,
|
|
||||||
Top,
|
|
||||||
ShadowTop,
|
|
||||||
Solid,
|
|
||||||
Char,
|
|
||||||
}
|
|
||||||
|
|
||||||
type LogoTemplateCell = {
|
|
||||||
x: number
|
|
||||||
y: number
|
|
||||||
kind: LogoCellKind
|
|
||||||
charCode: number
|
|
||||||
attributes: number
|
|
||||||
topDist: number
|
|
||||||
bottomDist: number
|
|
||||||
}
|
|
||||||
|
|
||||||
const LOGO_TEMPLATE: LogoTemplateCell[] = LOGO_LINES.flatMap((line, y) =>
|
|
||||||
Array.from(line)
|
|
||||||
.map((char, x) => {
|
|
||||||
if (char === " ") return
|
|
||||||
const kind =
|
|
||||||
char === "_"
|
|
||||||
? LogoCellKind.Background
|
|
||||||
: char === "^"
|
|
||||||
? LogoCellKind.Top
|
|
||||||
: char === "~"
|
|
||||||
? LogoCellKind.ShadowTop
|
|
||||||
: char === "█"
|
|
||||||
? LogoCellKind.Solid
|
|
||||||
: LogoCellKind.Char
|
|
||||||
return {
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
kind,
|
|
||||||
charCode: char.codePointAt(0) ?? SPACE,
|
|
||||||
attributes: x > LOGO_LEFT_WIDTH ? TextAttributes.BOLD : 0,
|
|
||||||
topDist: Math.hypot(x + 0.5 - LOGO_WIDTH / 2, y * 2 - LOGO_HEIGHT),
|
|
||||||
bottomDist: Math.hypot(x + 0.5 - LOGO_WIDTH / 2, y * 2 + 1 - LOGO_HEIGHT),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter((cell): cell is LogoTemplateCell => !!cell),
|
|
||||||
)
|
|
||||||
|
|
||||||
export type Rgb = [number, number, number]
|
|
||||||
|
|
||||||
export type GoUpsellArtRenderOptions = {
|
|
||||||
deltaTime?: number
|
|
||||||
rgb?: boolean
|
|
||||||
cache?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const CACHE_FRAME_COUNT = Math.round(PERIOD / (1000 / 30))
|
|
||||||
const CACHE_FRAMES_PER_RENDER = 1
|
|
||||||
|
|
||||||
export function toRgb(color: RGBA): Rgb {
|
|
||||||
const [r, g, b] = color.toInts()
|
|
||||||
return [r, g, b]
|
|
||||||
}
|
|
||||||
|
|
||||||
function clamp(n: number) {
|
|
||||||
return Math.max(0, Math.min(1, n))
|
|
||||||
}
|
|
||||||
|
|
||||||
function writeRgb(buffer: Uint16Array, offset: number, r: number, g: number, b: number, a = 255) {
|
|
||||||
buffer[offset] = r
|
|
||||||
buffer[offset + 1] = g
|
|
||||||
buffer[offset + 2] = b
|
|
||||||
buffer[offset + 3] = a
|
|
||||||
}
|
|
||||||
|
|
||||||
function mixChannel(base: number, overlay: number, alpha: number) {
|
|
||||||
return Math.round(base + (overlay - base) * clamp(alpha))
|
|
||||||
}
|
|
||||||
|
|
||||||
function writeLogoTint(
|
|
||||||
buffer: Uint16Array,
|
|
||||||
offset: number,
|
|
||||||
base: Rgb,
|
|
||||||
primary: Rgb,
|
|
||||||
primaryMix: number,
|
|
||||||
peakMix: number,
|
|
||||||
) {
|
|
||||||
const p = clamp(primaryMix)
|
|
||||||
const q = clamp(peakMix)
|
|
||||||
const r = mixChannel(mixChannel(base[0], primary[0], p), 255, q)
|
|
||||||
const g = mixChannel(mixChannel(base[1], primary[1], p), 255, q)
|
|
||||||
const b = mixChannel(mixChannel(base[2], primary[2], p), 255, q)
|
|
||||||
writeRgb(buffer, offset, r, g, b)
|
|
||||||
}
|
|
||||||
|
|
||||||
function sameRgb(a: Rgb, b: Rgb) {
|
|
||||||
return a[0] === b[0] && a[1] === b[1] && a[2] === b[2]
|
|
||||||
}
|
|
||||||
|
|
||||||
export class GoUpsellArtPainter {
|
|
||||||
private panelRgb: Rgb = [0, 0, 0]
|
|
||||||
private primaryRgb: Rgb = [255, 255, 255]
|
|
||||||
private logoBaseRgb: Rgb = [180, 180, 180]
|
|
||||||
private elapsed = 0
|
|
||||||
private distances = new Float32Array(0)
|
|
||||||
private edgeFalloff = new Float32Array(0)
|
|
||||||
private geometryWidth = 0
|
|
||||||
private geometryHeight = 0
|
|
||||||
private reach = 1
|
|
||||||
private logoX = 0
|
|
||||||
private logoY = 0
|
|
||||||
private logoIndexes = new Int32Array(0)
|
|
||||||
private logoRgb: boolean | undefined
|
|
||||||
private pulsePeak = 0
|
|
||||||
private pulsePrimary = 0
|
|
||||||
private cacheDirty = true
|
|
||||||
private frameCache: Array<{ fg: Uint16Array; bg: Uint16Array }> = []
|
|
||||||
private cacheBuildIndex = 0
|
|
||||||
|
|
||||||
setBackgroundPanel(value: RGBA | Rgb | undefined) {
|
|
||||||
if (!value) return false
|
|
||||||
const next = value instanceof RGBA ? toRgb(value) : value
|
|
||||||
if (sameRgb(this.panelRgb, next)) return false
|
|
||||||
this.panelRgb = next
|
|
||||||
this.invalidateCache()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
setLogoBase(value: RGBA | Rgb | undefined) {
|
|
||||||
if (!value) return false
|
|
||||||
const next = value instanceof RGBA ? toRgb(value) : value
|
|
||||||
if (sameRgb(this.logoBaseRgb, next)) return false
|
|
||||||
this.logoBaseRgb = next
|
|
||||||
this.invalidateCache()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
setPrimary(value: RGBA | Rgb | undefined) {
|
|
||||||
if (!value) return false
|
|
||||||
const next = value instanceof RGBA ? toRgb(value) : value
|
|
||||||
if (sameRgb(this.primaryRgb, next)) return false
|
|
||||||
this.primaryRgb = next
|
|
||||||
this.invalidateCache()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
render(frameBuffer: OptimizedBuffer, options: GoUpsellArtRenderOptions = {}) {
|
|
||||||
const rgb = options.rgb === true
|
|
||||||
this.elapsed = (this.elapsed + (options.deltaTime ?? 0)) % PERIOD
|
|
||||||
this.rebuildGeometry(frameBuffer, rgb)
|
|
||||||
if (options.cache !== false) {
|
|
||||||
this.drawCached(frameBuffer, rgb)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.drawBackground(frameBuffer, this.elapsed)
|
|
||||||
this.drawLogo(frameBuffer, this.elapsed, rgb)
|
|
||||||
}
|
|
||||||
|
|
||||||
private invalidateCache() {
|
|
||||||
this.cacheDirty = true
|
|
||||||
this.cacheBuildIndex = 0
|
|
||||||
this.frameCache = []
|
|
||||||
}
|
|
||||||
|
|
||||||
private rebuildGeometry(frameBuffer: OptimizedBuffer, rgb: boolean) {
|
|
||||||
const width = frameBuffer.width
|
|
||||||
const height = frameBuffer.height
|
|
||||||
const geometryChanged = width !== this.geometryWidth || height !== this.geometryHeight
|
|
||||||
const logoTemplateChanged = this.logoRgb !== rgb
|
|
||||||
if (!geometryChanged && !logoTemplateChanged) return
|
|
||||||
|
|
||||||
if (geometryChanged) {
|
|
||||||
this.geometryWidth = width
|
|
||||||
this.geometryHeight = height
|
|
||||||
this.logoX = Math.max(0, Math.floor((width - LOGO_WIDTH) / 2))
|
|
||||||
this.logoY = Math.max(
|
|
||||||
0,
|
|
||||||
Math.min(Math.max(0, height - LOGO_HEIGHT), Math.round((height - LOGO_HEIGHT) / 2) + LOGO_TOP_BIAS),
|
|
||||||
)
|
|
||||||
|
|
||||||
const centerX = this.logoX + LOGO_WIDTH / 2
|
|
||||||
const centerY = this.logoY + LOGO_HEIGHT / 2
|
|
||||||
this.reach = Math.hypot(Math.max(centerX, width - centerX), Math.max(centerY, height - centerY) * 2) + TAIL
|
|
||||||
this.distances = new Float32Array(width * height)
|
|
||||||
this.edgeFalloff = new Float32Array(width * height)
|
|
||||||
|
|
||||||
for (let y = 0; y < height; y++) {
|
|
||||||
for (let x = 0; x < width; x++) {
|
|
||||||
const index = y * width + x
|
|
||||||
const dist = Math.hypot(x + 0.5 - centerX, (y + 0.5 - centerY) * 2)
|
|
||||||
this.distances[index] = dist
|
|
||||||
this.edgeFalloff[index] = Math.max(0, 1 - (dist / (this.reach * 0.85)) ** 2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logoRgb = rgb
|
|
||||||
this.invalidateCache()
|
|
||||||
this.rebuildCellTemplate(frameBuffer, rgb)
|
|
||||||
}
|
|
||||||
|
|
||||||
private drawCached(frameBuffer: OptimizedBuffer, rgb: boolean) {
|
|
||||||
if (this.cacheDirty) this.startFrameCache(frameBuffer, rgb)
|
|
||||||
if (this.cacheBuildIndex < CACHE_FRAME_COUNT) {
|
|
||||||
this.buildFrameCache(frameBuffer, rgb)
|
|
||||||
this.drawBackground(frameBuffer, this.elapsed)
|
|
||||||
this.drawLogo(frameBuffer, this.elapsed, rgb)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const frame = this.frameCache[Math.floor((this.elapsed / PERIOD) * CACHE_FRAME_COUNT) % CACHE_FRAME_COUNT]
|
|
||||||
if (frame) {
|
|
||||||
frameBuffer.buffers.fg.set(frame.fg)
|
|
||||||
frameBuffer.buffers.bg.set(frame.bg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private startFrameCache(frameBuffer: OptimizedBuffer, rgb: boolean) {
|
|
||||||
this.frameCache = []
|
|
||||||
this.cacheBuildIndex = 0
|
|
||||||
this.rebuildCellTemplate(frameBuffer, rgb)
|
|
||||||
this.cacheDirty = false
|
|
||||||
}
|
|
||||||
|
|
||||||
private buildFrameCache(frameBuffer: OptimizedBuffer, rgb: boolean) {
|
|
||||||
const end = Math.min(CACHE_FRAME_COUNT, this.cacheBuildIndex + CACHE_FRAMES_PER_RENDER)
|
|
||||||
for (; this.cacheBuildIndex < end; this.cacheBuildIndex++) {
|
|
||||||
const t = (this.cacheBuildIndex / CACHE_FRAME_COUNT) * PERIOD
|
|
||||||
this.drawBackground(frameBuffer, t)
|
|
||||||
this.drawLogo(frameBuffer, t, rgb)
|
|
||||||
this.frameCache.push({
|
|
||||||
fg: new Uint16Array(frameBuffer.buffers.fg),
|
|
||||||
bg: new Uint16Array(frameBuffer.buffers.bg),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private rebuildCellTemplate(frameBuffer: OptimizedBuffer, rgb: boolean) {
|
|
||||||
const buffers = frameBuffer.buffers
|
|
||||||
buffers.char.fill(SPACE)
|
|
||||||
buffers.attributes.fill(0)
|
|
||||||
|
|
||||||
if (this.geometryWidth < LOGO_WIDTH || this.geometryHeight < LOGO_HEIGHT) {
|
|
||||||
this.logoIndexes = new Int32Array(0)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logoIndexes = new Int32Array(LOGO_TEMPLATE.length)
|
|
||||||
for (let i = 0; i < LOGO_TEMPLATE.length; i++) {
|
|
||||||
const cell = LOGO_TEMPLATE[i]!
|
|
||||||
const index = (this.logoY + cell.y) * this.geometryWidth + this.logoX + cell.x
|
|
||||||
this.logoIndexes[i] = index
|
|
||||||
buffers.attributes[index] = cell.attributes
|
|
||||||
buffers.char[index] =
|
|
||||||
cell.kind === LogoCellKind.Background
|
|
||||||
? SPACE
|
|
||||||
: cell.kind === LogoCellKind.Top || cell.kind === LogoCellKind.ShadowTop
|
|
||||||
? TOP_HALF
|
|
||||||
: cell.kind === LogoCellKind.Solid
|
|
||||||
? rgb
|
|
||||||
? TOP_HALF
|
|
||||||
: FULL_BLOCK
|
|
||||||
: cell.charCode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private drawBackground(frameBuffer: OptimizedBuffer, t: number) {
|
|
||||||
const buffers = frameBuffer.buffers
|
|
||||||
const fg = buffers.fg
|
|
||||||
const bg = buffers.bg
|
|
||||||
const distances = this.distances
|
|
||||||
const edgeFalloff = this.edgeFalloff
|
|
||||||
const baseR = this.panelRgb[0]
|
|
||||||
const baseG = this.panelRgb[1]
|
|
||||||
const baseB = this.panelRgb[2]
|
|
||||||
const deltaR = this.primaryRgb[0] - baseR
|
|
||||||
const deltaG = this.primaryRgb[1] - baseG
|
|
||||||
const deltaB = this.primaryRgb[2] - baseB
|
|
||||||
const breath = (0.5 + 0.5 * Math.sin(t * BREATH_SPEED)) * BREATH_AMP
|
|
||||||
|
|
||||||
const phase0 = (t / PERIOD - PHASE_OFFSET + 1) % 1
|
|
||||||
const phase1 = (t / PERIOD + 1 / RINGS - PHASE_OFFSET + 1) % 1
|
|
||||||
const phase2 = (t / PERIOD + 2 / RINGS - PHASE_OFFSET + 1) % 1
|
|
||||||
const envelope0 = Math.sin(phase0 * Math.PI)
|
|
||||||
const envelope1 = Math.sin(phase1 * Math.PI)
|
|
||||||
const envelope2 = Math.sin(phase2 * Math.PI)
|
|
||||||
const eased0 = envelope0 * envelope0 * (3 - 2 * envelope0)
|
|
||||||
const eased1 = envelope1 * envelope1 * (3 - 2 * envelope1)
|
|
||||||
const eased2 = envelope2 * envelope2 * (3 - 2 * envelope2)
|
|
||||||
const head0 = phase0 * this.reach
|
|
||||||
const head1 = phase1 * this.reach
|
|
||||||
const head2 = phase2 * this.reach
|
|
||||||
|
|
||||||
for (let index = 0; index < distances.length; index++) {
|
|
||||||
const dist = distances[index]
|
|
||||||
const delta0 = dist - head0
|
|
||||||
const abs0 = delta0 < 0 ? -delta0 : delta0
|
|
||||||
const crest0 = abs0 < WIDTH ? 0.5 + 0.5 * Math.cos((delta0 / WIDTH) * Math.PI) : 0
|
|
||||||
const tail0 = delta0 < 0 && delta0 > -TAIL ? (1 + delta0 * TAIL_SCALE) ** 2.3 : 0
|
|
||||||
|
|
||||||
const delta1 = dist - head1
|
|
||||||
const abs1 = delta1 < 0 ? -delta1 : delta1
|
|
||||||
const crest1 = abs1 < WIDTH ? 0.5 + 0.5 * Math.cos((delta1 / WIDTH) * Math.PI) : 0
|
|
||||||
const tail1 = delta1 < 0 && delta1 > -TAIL ? (1 + delta1 * TAIL_SCALE) ** 2.3 : 0
|
|
||||||
|
|
||||||
const delta2 = dist - head2
|
|
||||||
const abs2 = delta2 < 0 ? -delta2 : delta2
|
|
||||||
const crest2 = abs2 < WIDTH ? 0.5 + 0.5 * Math.cos((delta2 / WIDTH) * Math.PI) : 0
|
|
||||||
const tail2 = delta2 < 0 && delta2 > -TAIL ? (1 + delta2 * TAIL_SCALE) ** 2.3 : 0
|
|
||||||
|
|
||||||
const level =
|
|
||||||
(crest0 * AMP + tail0 * TAIL_AMP) * eased0 +
|
|
||||||
(crest1 * AMP + tail1 * TAIL_AMP) * eased1 +
|
|
||||||
(crest2 * AMP + tail2 * TAIL_AMP) * eased2
|
|
||||||
const rawStrength = (level * RING_SCALE + breath) * edgeFalloff[index]
|
|
||||||
const strength = (rawStrength > 1 ? 1 : rawStrength) * 0.7
|
|
||||||
const offset = index * 4
|
|
||||||
const r = Math.round(baseR + deltaR * strength)
|
|
||||||
const g = Math.round(baseG + deltaG * strength)
|
|
||||||
const b = Math.round(baseB + deltaB * strength)
|
|
||||||
bg[offset] = fg[offset] = r
|
|
||||||
bg[offset + 1] = fg[offset + 1] = g
|
|
||||||
bg[offset + 2] = fg[offset + 2] = b
|
|
||||||
bg[offset + 3] = fg[offset + 3] = 255
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private setLogoPulse(dist: number, head0: number, eased0: number, head1: number, eased1: number) {
|
|
||||||
let peak = 0.04
|
|
||||||
let primary = 0
|
|
||||||
|
|
||||||
const delta0 = dist - head0
|
|
||||||
const core0 = Math.exp(-(Math.abs(delta0 / 1.2) ** 1.8))
|
|
||||||
const soft0 = Math.exp(-(Math.abs(delta0 / 7) ** 1.6))
|
|
||||||
const tail0 = delta0 < 0 && delta0 > -7 ? (1 + delta0 / 7) ** 2.6 : 0
|
|
||||||
peak += core0 * 0.65 * eased0
|
|
||||||
primary += (soft0 * 0.16 + tail0 * 0.22) * eased0
|
|
||||||
|
|
||||||
const delta1 = dist - head1
|
|
||||||
const core1 = Math.exp(-(Math.abs(delta1 / 1.2) ** 1.8))
|
|
||||||
const soft1 = Math.exp(-(Math.abs(delta1 / 7) ** 1.6))
|
|
||||||
const tail1 = delta1 < 0 && delta1 > -7 ? (1 + delta1 / 7) ** 2.6 : 0
|
|
||||||
peak += core1 * 0.65 * eased1
|
|
||||||
primary += (soft1 * 0.16 + tail1 * 0.22) * eased1
|
|
||||||
|
|
||||||
this.pulsePeak = peak > 1 ? 1 : peak
|
|
||||||
this.pulsePrimary = primary > 1 ? 1 : primary
|
|
||||||
}
|
|
||||||
|
|
||||||
private drawLogo(frameBuffer: OptimizedBuffer, t: number, rgb: boolean) {
|
|
||||||
if (this.logoIndexes.length === 0) return
|
|
||||||
|
|
||||||
const buffers = frameBuffer.buffers
|
|
||||||
const fg = buffers.fg
|
|
||||||
const bg = buffers.bg
|
|
||||||
const shadow: Rgb = [
|
|
||||||
mixChannel(this.panelRgb[0], this.logoBaseRgb[0], 0.25),
|
|
||||||
mixChannel(this.panelRgb[1], this.logoBaseRgb[1], 0.25),
|
|
||||||
mixChannel(this.panelRgb[2], this.logoBaseRgb[2], 0.25),
|
|
||||||
]
|
|
||||||
const phase0 = (t / PERIOD) % 1
|
|
||||||
const phase1 = (t / PERIOD + 0.5) % 1
|
|
||||||
const envelope0 = Math.sin(phase0 * Math.PI)
|
|
||||||
const envelope1 = Math.sin(phase1 * Math.PI)
|
|
||||||
const eased0 = envelope0 * envelope0 * (3 - 2 * envelope0)
|
|
||||||
const eased1 = envelope1 * envelope1 * (3 - 2 * envelope1)
|
|
||||||
const head0 = phase0 * LOGO_REACH
|
|
||||||
const head1 = phase1 * LOGO_REACH
|
|
||||||
|
|
||||||
for (let i = 0; i < LOGO_TEMPLATE.length; i++) {
|
|
||||||
const cell = LOGO_TEMPLATE[i]!
|
|
||||||
const index = this.logoIndexes[i]!
|
|
||||||
const offset = index * 4
|
|
||||||
this.setLogoPulse(cell.topDist, head0, eased0, head1, eased1)
|
|
||||||
const topPeak = this.pulsePeak
|
|
||||||
const topPrimary = this.pulsePrimary
|
|
||||||
this.setLogoPulse(cell.bottomDist, head0, eased0, head1, eased1)
|
|
||||||
const bottomPeak = this.pulsePeak
|
|
||||||
const bottomPrimary = this.pulsePrimary
|
|
||||||
|
|
||||||
if (cell.kind === LogoCellKind.Background) {
|
|
||||||
writeLogoTint(bg, offset, shadow, this.primaryRgb, 0, Math.max(topPeak, bottomPeak) * 0.18)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cell.kind === LogoCellKind.Top) {
|
|
||||||
writeLogoTint(fg, offset, this.logoBaseRgb, this.primaryRgb, topPrimary, topPeak)
|
|
||||||
writeLogoTint(bg, offset, shadow, this.primaryRgb, 0, bottomPeak * 0.18)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cell.kind === LogoCellKind.ShadowTop) {
|
|
||||||
writeLogoTint(fg, offset, shadow, this.primaryRgb, 0, topPeak * 0.18)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cell.kind === LogoCellKind.Solid && rgb) {
|
|
||||||
writeLogoTint(fg, offset, this.logoBaseRgb, this.primaryRgb, topPrimary, topPeak)
|
|
||||||
writeLogoTint(bg, offset, this.logoBaseRgb, this.primaryRgb, bottomPrimary, bottomPeak)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
writeLogoTint(
|
|
||||||
fg,
|
|
||||||
offset,
|
|
||||||
this.logoBaseRgb,
|
|
||||||
this.primaryRgb,
|
|
||||||
(topPrimary + bottomPrimary) / 2,
|
|
||||||
(topPeak + bottomPeak) / 2,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,102 +0,0 @@
|
||||||
import {
|
|
||||||
FrameBufferRenderable,
|
|
||||||
RGBA,
|
|
||||||
type OptimizedBuffer,
|
|
||||||
type RenderContext,
|
|
||||||
type RenderableOptions,
|
|
||||||
} from "@opentui/core"
|
|
||||||
import { extend, useRenderer } from "@opentui/solid"
|
|
||||||
import { onCleanup, onMount } from "solid-js"
|
|
||||||
import { useTheme, useThemes } from "../context/theme"
|
|
||||||
import { tint } from "../theme/color"
|
|
||||||
import { GoUpsellArtPainter } from "./bg-pulse-render"
|
|
||||||
|
|
||||||
type GoUpsellArtOptions = RenderableOptions<FrameBufferRenderable> & {
|
|
||||||
backgroundPanel?: RGBA
|
|
||||||
primary?: RGBA
|
|
||||||
logoBase?: RGBA
|
|
||||||
}
|
|
||||||
|
|
||||||
class GoUpsellArtRenderable extends FrameBufferRenderable {
|
|
||||||
private painter = new GoUpsellArtPainter()
|
|
||||||
|
|
||||||
constructor(ctx: RenderContext, options: GoUpsellArtOptions = {}) {
|
|
||||||
const width = typeof options.width === "number" ? options.width : 1
|
|
||||||
const height = typeof options.height === "number" ? options.height : 1
|
|
||||||
super(ctx, {
|
|
||||||
...options,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
live: options.live ?? true,
|
|
||||||
respectAlpha: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (options.width !== undefined && typeof options.width !== "number") this.width = options.width
|
|
||||||
if (options.height !== undefined && typeof options.height !== "number") this.height = options.height
|
|
||||||
this.painter.setBackgroundPanel(options.backgroundPanel)
|
|
||||||
this.painter.setPrimary(options.primary)
|
|
||||||
this.painter.setLogoBase(options.logoBase)
|
|
||||||
}
|
|
||||||
|
|
||||||
set backgroundPanel(value: RGBA | undefined) {
|
|
||||||
if (this.painter.setBackgroundPanel(value)) this.requestRender()
|
|
||||||
}
|
|
||||||
|
|
||||||
set logoBase(value: RGBA | undefined) {
|
|
||||||
if (this.painter.setLogoBase(value)) this.requestRender()
|
|
||||||
}
|
|
||||||
|
|
||||||
set primary(value: RGBA | undefined) {
|
|
||||||
if (this.painter.setPrimary(value)) this.requestRender()
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override renderSelf(buffer: OptimizedBuffer, deltaTime = 0): void {
|
|
||||||
if (!this.visible || this.isDestroyed) return
|
|
||||||
|
|
||||||
this.painter.render(this.frameBuffer, {
|
|
||||||
deltaTime,
|
|
||||||
rgb: this._ctx.capabilities?.rgb === true,
|
|
||||||
})
|
|
||||||
super.renderSelf(buffer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module "@opentui/solid" {
|
|
||||||
interface OpenTUIComponents {
|
|
||||||
go_upsell_art: typeof GoUpsellArtRenderable
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extend({ go_upsell_art: GoUpsellArtRenderable })
|
|
||||||
|
|
||||||
export function BgPulse() {
|
|
||||||
const themes = useThemes()
|
|
||||||
const theme = useTheme("elevated")
|
|
||||||
const mode = themes.mode
|
|
||||||
const renderer = useRenderer()
|
|
||||||
let targetFps = renderer.targetFps
|
|
||||||
let maxFps = renderer.maxFps
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
targetFps = renderer.targetFps
|
|
||||||
maxFps = renderer.maxFps
|
|
||||||
renderer.targetFps = 30
|
|
||||||
renderer.maxFps = 30
|
|
||||||
})
|
|
||||||
|
|
||||||
onCleanup(() => {
|
|
||||||
renderer.targetFps = targetFps
|
|
||||||
renderer.maxFps = maxFps
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<go_upsell_art
|
|
||||||
width="100%"
|
|
||||||
height="100%"
|
|
||||||
backgroundPanel={theme.background.default}
|
|
||||||
primary={theme.hue.interactive[mode() === "light" ? 800 : 200]}
|
|
||||||
logoBase={tint(theme.background.default, theme.text.default, 0.62)}
|
|
||||||
live
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,164 +0,0 @@
|
||||||
import { RGBA, TextAttributes } from "@opentui/core"
|
|
||||||
import open from "open"
|
|
||||||
import { createSignal } from "solid-js"
|
|
||||||
import { Keymap } from "../context/keymap"
|
|
||||||
import { useTheme } from "../context/theme"
|
|
||||||
import { useDialog, type DialogContext } from "../ui/dialog"
|
|
||||||
import { Link } from "../ui/link"
|
|
||||||
import { BgPulse } from "./bg-pulse"
|
|
||||||
|
|
||||||
const GO_URL = "https://opencode.ai/go"
|
|
||||||
const PAD_X = 3
|
|
||||||
const PAD_TOP_OUTER = 1
|
|
||||||
const FOREGROUND_ALPHA = 186
|
|
||||||
|
|
||||||
export type DialogRetryActionProps = {
|
|
||||||
title: string
|
|
||||||
message: string
|
|
||||||
label: string
|
|
||||||
link?: string
|
|
||||||
onClose?: (dontShowAgain?: boolean) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
function runAction(props: DialogRetryActionProps, dialog: ReturnType<typeof useDialog>) {
|
|
||||||
if (props.link) open(props.link).catch(() => {})
|
|
||||||
props.onClose?.()
|
|
||||||
dialog.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
function dismiss(props: DialogRetryActionProps, dialog: ReturnType<typeof useDialog>) {
|
|
||||||
props.onClose?.(true)
|
|
||||||
dialog.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
function panelOverlay(color: RGBA) {
|
|
||||||
const [r, g, b] = color.toInts()
|
|
||||||
return RGBA.fromInts(r, g, b, FOREGROUND_ALPHA)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DialogRetryAction(props: DialogRetryActionProps) {
|
|
||||||
const dialog = useDialog()
|
|
||||||
const theme = useTheme("elevated")
|
|
||||||
const showGoTreatment = () => props.link === GO_URL
|
|
||||||
const textBg = () => (showGoTreatment() ? panelOverlay(theme.background.default) : undefined)
|
|
||||||
const [selected, setSelected] = createSignal<"dismiss" | "action">("action")
|
|
||||||
|
|
||||||
Keymap.createLayer(() => ({
|
|
||||||
mode: "modal",
|
|
||||||
commands: [
|
|
||||||
{
|
|
||||||
bind: "left",
|
|
||||||
title: "Previous retry option",
|
|
||||||
group: "Dialog",
|
|
||||||
run: () => setSelected((value) => (value === "action" ? "dismiss" : "action")),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
bind: "right",
|
|
||||||
title: "Next retry option",
|
|
||||||
group: "Dialog",
|
|
||||||
run: () => setSelected((value) => (value === "action" ? "dismiss" : "action")),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
bind: "tab",
|
|
||||||
title: "Next retry option",
|
|
||||||
group: "Dialog",
|
|
||||||
run: () => setSelected((value) => (value === "action" ? "dismiss" : "action")),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
bind: "return",
|
|
||||||
title: "Confirm retry option",
|
|
||||||
group: "Dialog",
|
|
||||||
run: () => {
|
|
||||||
if (selected() === "action") runAction(props, dialog)
|
|
||||||
else dismiss(props, dialog)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}))
|
|
||||||
|
|
||||||
return (
|
|
||||||
<box>
|
|
||||||
{showGoTreatment() ? (
|
|
||||||
<box position="absolute" top={-PAD_TOP_OUTER} left={0} right={0} bottom={0} zIndex={0}>
|
|
||||||
<BgPulse />
|
|
||||||
</box>
|
|
||||||
) : null}
|
|
||||||
<box zIndex={1} paddingLeft={PAD_X} paddingRight={PAD_X} paddingBottom={1} gap={1}>
|
|
||||||
<box flexDirection="row" justifyContent="space-between">
|
|
||||||
<text attributes={TextAttributes.BOLD} fg={theme.text.default} bg={textBg()}>
|
|
||||||
{props.title}
|
|
||||||
</text>
|
|
||||||
<text fg={theme.text.subdued} bg={textBg()} onMouseUp={() => dialog.clear()}>
|
|
||||||
esc
|
|
||||||
</text>
|
|
||||||
</box>
|
|
||||||
<box gap={0}>
|
|
||||||
<text fg={theme.text.subdued} bg={textBg()}>
|
|
||||||
{props.message}
|
|
||||||
</text>
|
|
||||||
</box>
|
|
||||||
{props.link ? (
|
|
||||||
showGoTreatment() ? (
|
|
||||||
<box alignItems="center" justifyContent="flex-end" height={7} paddingBottom={1}>
|
|
||||||
<Link href={props.link} fg={theme.markdown.link} bg={textBg()} wrapMode="none" />
|
|
||||||
</box>
|
|
||||||
) : (
|
|
||||||
<box width="100%" flexDirection="row" justifyContent="center" paddingBottom={1}>
|
|
||||||
<Link href={props.link} fg={theme.markdown.link} wrapMode="none" />
|
|
||||||
</box>
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<box paddingBottom={1} />
|
|
||||||
)}
|
|
||||||
<box flexDirection="row" justifyContent="space-between">
|
|
||||||
<box
|
|
||||||
paddingLeft={2}
|
|
||||||
paddingRight={2}
|
|
||||||
backgroundColor={
|
|
||||||
selected() === "dismiss" ? theme.background.action.primary.focused : RGBA.fromInts(0, 0, 0, 0)
|
|
||||||
}
|
|
||||||
onMouseOver={() => setSelected("dismiss")}
|
|
||||||
onMouseUp={() => dismiss(props, dialog)}
|
|
||||||
>
|
|
||||||
<text
|
|
||||||
fg={selected() === "dismiss" ? theme.text.action.primary.focused : theme.text.subdued}
|
|
||||||
bg={selected() === "dismiss" ? undefined : textBg()}
|
|
||||||
attributes={selected() === "dismiss" ? TextAttributes.BOLD : undefined}
|
|
||||||
>
|
|
||||||
don't show again
|
|
||||||
</text>
|
|
||||||
</box>
|
|
||||||
<box
|
|
||||||
paddingLeft={2}
|
|
||||||
paddingRight={2}
|
|
||||||
backgroundColor={
|
|
||||||
selected() === "action" ? theme.background.action.primary.focused : RGBA.fromInts(0, 0, 0, 0)
|
|
||||||
}
|
|
||||||
onMouseOver={() => setSelected("action")}
|
|
||||||
onMouseUp={() => runAction(props, dialog)}
|
|
||||||
>
|
|
||||||
<text
|
|
||||||
fg={selected() === "action" ? theme.text.action.primary.focused : theme.text.default}
|
|
||||||
bg={selected() === "action" ? undefined : textBg()}
|
|
||||||
attributes={selected() === "action" ? TextAttributes.BOLD : undefined}
|
|
||||||
>
|
|
||||||
{props.label}
|
|
||||||
</text>
|
|
||||||
</box>
|
|
||||||
</box>
|
|
||||||
</box>
|
|
||||||
</box>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
DialogRetryAction.show = (
|
|
||||||
dialog: DialogContext,
|
|
||||||
props: Pick<DialogRetryActionProps, "title" | "message" | "label" | "link">,
|
|
||||||
) => {
|
|
||||||
return new Promise<boolean>((resolve) => {
|
|
||||||
dialog.replace(
|
|
||||||
() => <DialogRetryAction {...props} onClose={(dontShow) => resolve(dontShow ?? false)} />,
|
|
||||||
() => resolve(false),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
@ -1,113 +0,0 @@
|
||||||
import { TextAttributes } from "@opentui/core"
|
|
||||||
import { Keymap } from "../context/keymap"
|
|
||||||
import { useTheme } from "../context/theme"
|
|
||||||
import { useDialog } from "../ui/dialog"
|
|
||||||
import { createStore } from "solid-js/store"
|
|
||||||
import { For } from "solid-js"
|
|
||||||
|
|
||||||
export function DialogSessionDeleteFailed(props: {
|
|
||||||
session: string
|
|
||||||
workspace: string
|
|
||||||
onDelete?: () => boolean | void | Promise<boolean | void>
|
|
||||||
onRestore?: () => boolean | void | Promise<boolean | void>
|
|
||||||
onDone?: () => void
|
|
||||||
}) {
|
|
||||||
const dialog = useDialog()
|
|
||||||
const theme = useTheme("elevated")
|
|
||||||
const [store, setStore] = createStore({
|
|
||||||
active: "delete" as "delete" | "restore",
|
|
||||||
})
|
|
||||||
|
|
||||||
const options = [
|
|
||||||
{
|
|
||||||
id: "delete" as const,
|
|
||||||
title: "Delete workspace",
|
|
||||||
description: "Delete the workspace and all sessions attached to it.",
|
|
||||||
run: props.onDelete,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "restore" as const,
|
|
||||||
title: "Restore to new workspace",
|
|
||||||
description: "Try to restore this session into a new workspace.",
|
|
||||||
run: props.onRestore,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
async function confirm() {
|
|
||||||
const result = await options.find((item) => item.id === store.active)?.run?.()
|
|
||||||
if (result === false) return
|
|
||||||
props.onDone?.()
|
|
||||||
if (!props.onDone) dialog.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
Keymap.createLayer(() => ({
|
|
||||||
mode: "modal",
|
|
||||||
commands: [
|
|
||||||
{ bind: "return", title: "Confirm recovery option", group: "Dialog", run: () => void confirm() },
|
|
||||||
{ bind: "left", title: "Delete broken session", group: "Dialog", run: () => setStore("active", "delete") },
|
|
||||||
{ bind: "up", title: "Delete broken session", group: "Dialog", run: () => setStore("active", "delete") },
|
|
||||||
{
|
|
||||||
bind: "right",
|
|
||||||
title: "Restore broken session",
|
|
||||||
group: "Dialog",
|
|
||||||
run: () => setStore("active", "restore"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
bind: "down",
|
|
||||||
title: "Restore broken session",
|
|
||||||
group: "Dialog",
|
|
||||||
run: () => setStore("active", "restore"),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}))
|
|
||||||
|
|
||||||
return (
|
|
||||||
<box paddingLeft={2} paddingRight={2} gap={1}>
|
|
||||||
<box flexDirection="row" justifyContent="space-between">
|
|
||||||
<text attributes={TextAttributes.BOLD} fg={theme.text.default}>
|
|
||||||
Failed to Delete Session
|
|
||||||
</text>
|
|
||||||
<text fg={theme.text.subdued} onMouseUp={() => dialog.clear()}>
|
|
||||||
esc
|
|
||||||
</text>
|
|
||||||
</box>
|
|
||||||
<text fg={theme.text.subdued} wrapMode="word">
|
|
||||||
{`The session "${props.session}" could not be deleted because the workspace "${props.workspace}" is not available.`}
|
|
||||||
</text>
|
|
||||||
<text fg={theme.text.subdued} wrapMode="word">
|
|
||||||
Choose how you want to recover this broken workspace session.
|
|
||||||
</text>
|
|
||||||
<box flexDirection="column" paddingBottom={1} gap={1}>
|
|
||||||
<For each={options}>
|
|
||||||
{(item) => (
|
|
||||||
<box
|
|
||||||
flexDirection="column"
|
|
||||||
paddingLeft={1}
|
|
||||||
paddingRight={1}
|
|
||||||
paddingTop={1}
|
|
||||||
paddingBottom={1}
|
|
||||||
backgroundColor={item.id === store.active ? theme.background.action.primary.focused : undefined}
|
|
||||||
onMouseUp={() => {
|
|
||||||
setStore("active", item.id)
|
|
||||||
void confirm()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<text
|
|
||||||
attributes={TextAttributes.BOLD}
|
|
||||||
fg={item.id === store.active ? theme.text.action.primary.focused : theme.text.default}
|
|
||||||
>
|
|
||||||
{item.title}
|
|
||||||
</text>
|
|
||||||
<text
|
|
||||||
fg={item.id === store.active ? theme.text.action.primary.focused : theme.text.subdued}
|
|
||||||
wrapMode="word"
|
|
||||||
>
|
|
||||||
{item.description}
|
|
||||||
</text>
|
|
||||||
</box>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</box>
|
|
||||||
</box>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
import { createMemo, createResource } from "solid-js"
|
|
||||||
import { DialogSelect } from "../ui/dialog-select"
|
|
||||||
import { useDialog } from "../ui/dialog"
|
|
||||||
import { useClient } from "../context/client"
|
|
||||||
import { useData } from "../context/data"
|
|
||||||
import { createStore } from "solid-js/store"
|
|
||||||
|
|
||||||
export function DialogTag(props: { onSelect?: (value: string) => void }) {
|
|
||||||
const client = useClient()
|
|
||||||
const dialog = useDialog()
|
|
||||||
const data = useData()
|
|
||||||
|
|
||||||
const [store] = createStore({
|
|
||||||
filter: "",
|
|
||||||
})
|
|
||||||
|
|
||||||
const [files] = createResource(
|
|
||||||
() => [store.filter],
|
|
||||||
async () => {
|
|
||||||
const result = await client.api.file
|
|
||||||
.find({
|
|
||||||
query: store.filter,
|
|
||||||
type: "file",
|
|
||||||
limit: 5,
|
|
||||||
location: {
|
|
||||||
directory: data.location.default().directory,
|
|
||||||
workspace: data.location.default().workspaceID,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.catch(() => undefined)
|
|
||||||
return result?.data.map((item) => item.path) ?? []
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
const options = createMemo(() =>
|
|
||||||
(files() ?? []).map((file) => ({
|
|
||||||
value: file,
|
|
||||||
title: file,
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DialogSelect
|
|
||||||
title="Autocomplete"
|
|
||||||
options={options()}
|
|
||||||
onSelect={(option) => {
|
|
||||||
props.onSelect?.(option.value)
|
|
||||||
dialog.clear()
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
import { createMemo } from "solid-js"
|
|
||||||
import { useData } from "./data"
|
|
||||||
import { abbreviateHome } from "../runtime"
|
|
||||||
import { useTuiPaths } from "./runtime"
|
|
||||||
|
|
||||||
export function useDirectory() {
|
|
||||||
const data = useData()
|
|
||||||
const paths = useTuiPaths()
|
|
||||||
return createMemo(() => {
|
|
||||||
const directory = data.location.info()?.directory ?? data.location.default().directory ?? paths.cwd
|
|
||||||
return abbreviateHome(directory, paths.home)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
import { DialogSelect } from "../../ui/dialog-select"
|
|
||||||
import { useRoute } from "../../context/route"
|
|
||||||
|
|
||||||
export function DialogSubagent(props: { sessionID: string }) {
|
|
||||||
const route = useRoute()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DialogSelect
|
|
||||||
title="Subagent Actions"
|
|
||||||
options={[
|
|
||||||
{
|
|
||||||
title: "Open",
|
|
||||||
value: "subagent.view",
|
|
||||||
description: "the subagent's session",
|
|
||||||
onSelect: (dialog) => {
|
|
||||||
route.navigate({
|
|
||||||
type: "session",
|
|
||||||
sessionID: props.sessionID,
|
|
||||||
})
|
|
||||||
dialog.clear()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,91 +0,0 @@
|
||||||
import { createMemo, Match, onCleanup, onMount, Show, Switch } from "solid-js"
|
|
||||||
import { useTheme } from "../../context/theme"
|
|
||||||
import { useData } from "../../context/data"
|
|
||||||
import { useDirectory } from "../../context/directory"
|
|
||||||
import { useConnected } from "../../component/use-connected"
|
|
||||||
import { createStore } from "solid-js/store"
|
|
||||||
import { useRoute } from "../../context/route"
|
|
||||||
import { usePermission } from "../../context/permission"
|
|
||||||
|
|
||||||
export function Footer() {
|
|
||||||
const theme = useTheme()
|
|
||||||
const data = useData()
|
|
||||||
const route = useRoute()
|
|
||||||
const permission = usePermission()
|
|
||||||
const mcp = createMemo(
|
|
||||||
() => (data.location.mcp.server.list() ?? []).filter((x) => x.status.status === "connected").length,
|
|
||||||
)
|
|
||||||
const mcpError = createMemo(() => (data.location.mcp.server.list() ?? []).some((x) => x.status.status === "failed"))
|
|
||||||
const permissions = createMemo(() => {
|
|
||||||
if (route.data.type !== "session") return []
|
|
||||||
return data.session.permission.list(route.data.sessionID) ?? []
|
|
||||||
})
|
|
||||||
const directory = useDirectory()
|
|
||||||
const connected = useConnected()
|
|
||||||
|
|
||||||
const [store, setStore] = createStore({
|
|
||||||
welcome: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
// Track all timeouts to ensure proper cleanup
|
|
||||||
const timeouts: ReturnType<typeof setTimeout>[] = []
|
|
||||||
|
|
||||||
function tick() {
|
|
||||||
if (connected()) return
|
|
||||||
if (!store.welcome) {
|
|
||||||
setStore("welcome", true)
|
|
||||||
timeouts.push(setTimeout(() => tick(), 5000))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (store.welcome) {
|
|
||||||
setStore("welcome", false)
|
|
||||||
timeouts.push(setTimeout(() => tick(), 10_000))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
timeouts.push(setTimeout(() => tick(), 10_000))
|
|
||||||
|
|
||||||
onCleanup(() => {
|
|
||||||
timeouts.forEach(clearTimeout)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<box flexDirection="row" justifyContent="space-between" gap={1} flexShrink={0}>
|
|
||||||
<text fg={theme.text.subdued}>{directory()}</text>
|
|
||||||
<box gap={2} flexDirection="row" flexShrink={0}>
|
|
||||||
<Switch>
|
|
||||||
<Match when={store.welcome}>
|
|
||||||
<text fg={theme.text.default}>
|
|
||||||
Get started <span style={{ fg: theme.text.subdued }}>/connect</span>
|
|
||||||
</text>
|
|
||||||
</Match>
|
|
||||||
<Match when={connected()}>
|
|
||||||
<Show when={permission.mode !== "auto" && permissions().length > 0}>
|
|
||||||
<text fg={theme.text.feedback.warning.default}>
|
|
||||||
<span style={{ fg: theme.text.feedback.warning.default }}>△</span> {permissions().length} Permission
|
|
||||||
{permissions().length > 1 ? "s" : ""}
|
|
||||||
</text>
|
|
||||||
</Show>
|
|
||||||
<Show when={mcp()}>
|
|
||||||
<text fg={theme.text.default}>
|
|
||||||
<Switch>
|
|
||||||
<Match when={mcpError()}>
|
|
||||||
<span style={{ fg: theme.text.feedback.error.default }}>⊙ </span>
|
|
||||||
</Match>
|
|
||||||
<Match when={true}>
|
|
||||||
<span style={{ fg: theme.text.feedback.success.default }}>⊙ </span>
|
|
||||||
</Match>
|
|
||||||
</Switch>
|
|
||||||
{mcp()} MCP
|
|
||||||
</text>
|
|
||||||
</Show>
|
|
||||||
<text fg={theme.text.subdued}>/status</text>
|
|
||||||
</Match>
|
|
||||||
</Switch>
|
|
||||||
</box>
|
|
||||||
</box>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,115 +0,0 @@
|
||||||
import { createMemo, createSignal, Show } from "solid-js"
|
|
||||||
import { useRouteData } from "../../context/route"
|
|
||||||
import { useData } from "../../context/data"
|
|
||||||
import { useTheme } from "../../context/theme"
|
|
||||||
import { SplitBorder } from "../../ui/border"
|
|
||||||
import { Locale } from "../../util/locale"
|
|
||||||
import { useTerminalDimensions } from "@opentui/solid"
|
|
||||||
import { Keymap } from "../../context/keymap"
|
|
||||||
import { contextUsage, formatContextUsage } from "../../util/session"
|
|
||||||
|
|
||||||
const money = new Intl.NumberFormat("en-US", {
|
|
||||||
style: "currency",
|
|
||||||
currency: "USD",
|
|
||||||
})
|
|
||||||
|
|
||||||
export function SubagentFooter() {
|
|
||||||
const route = useRouteData("session")
|
|
||||||
const data = useData()
|
|
||||||
const session = createMemo(() => data.session.get(route.sessionID))
|
|
||||||
|
|
||||||
const subagentInfo = createMemo(() => {
|
|
||||||
const s = session()
|
|
||||||
if (!s) return "Subagent"
|
|
||||||
const agentMatch = s.title.match(/@(\w+) subagent/)
|
|
||||||
return agentMatch ? Locale.titlecase(agentMatch[1]) : "Subagent"
|
|
||||||
})
|
|
||||||
|
|
||||||
const usage = createMemo(() => {
|
|
||||||
const current = session()
|
|
||||||
if (!current) return
|
|
||||||
const cost = current.cost
|
|
||||||
const formattedCost = cost > 0 ? money.format(cost) : undefined
|
|
||||||
const context = contextUsage(
|
|
||||||
data.session.message.list(route.sessionID),
|
|
||||||
data.location.model.list(current.location),
|
|
||||||
current.revert?.messageID,
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
context: context ? formatContextUsage(context.tokens, context.percent) : undefined,
|
|
||||||
cost: formattedCost,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const theme = useTheme("elevated")
|
|
||||||
const keymap = Keymap.use()
|
|
||||||
const shortcuts = Keymap.useShortcuts()
|
|
||||||
const [hover, setHover] = createSignal<"parent" | "prev" | "next" | null>(null)
|
|
||||||
useTerminalDimensions()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<box flexShrink={0}>
|
|
||||||
<box
|
|
||||||
paddingTop={1}
|
|
||||||
paddingBottom={1}
|
|
||||||
paddingLeft={2}
|
|
||||||
paddingRight={1}
|
|
||||||
{...SplitBorder}
|
|
||||||
border={["left"]}
|
|
||||||
borderColor={theme.border.default}
|
|
||||||
flexShrink={0}
|
|
||||||
backgroundColor={theme.background.default}
|
|
||||||
>
|
|
||||||
<box flexDirection="row" justifyContent="space-between" gap={1}>
|
|
||||||
<box flexDirection="row" gap={1}>
|
|
||||||
<text fg={theme.text.default}>
|
|
||||||
<b>{subagentInfo()}</b>
|
|
||||||
</text>
|
|
||||||
<Show when={usage()}>
|
|
||||||
{(item) => (
|
|
||||||
<text fg={theme.text.subdued} wrapMode="none">
|
|
||||||
{[item().context, item().cost].filter(Boolean).join(" · ")}
|
|
||||||
</text>
|
|
||||||
)}
|
|
||||||
</Show>
|
|
||||||
</box>
|
|
||||||
<box flexDirection="row" gap={2}>
|
|
||||||
<box
|
|
||||||
onMouseOver={() => setHover("parent")}
|
|
||||||
onMouseOut={() => setHover(null)}
|
|
||||||
onMouseUp={() => keymap.dispatch("session.parent")}
|
|
||||||
backgroundColor={
|
|
||||||
hover() === "parent" ? theme.background.action.primary.hovered : theme.background.default
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<text fg={theme.text.default}>
|
|
||||||
Parent <span style={{ fg: theme.text.subdued }}>{shortcuts.get("session.parent")}</span>
|
|
||||||
</text>
|
|
||||||
</box>
|
|
||||||
<box
|
|
||||||
onMouseOver={() => setHover("prev")}
|
|
||||||
onMouseOut={() => setHover(null)}
|
|
||||||
onMouseUp={() => keymap.dispatch("session.child.previous")}
|
|
||||||
backgroundColor={hover() === "prev" ? theme.background.action.primary.hovered : theme.background.default}
|
|
||||||
>
|
|
||||||
<text fg={theme.text.default}>
|
|
||||||
Prev <span style={{ fg: theme.text.subdued }}>{shortcuts.get("session.child.previous")}</span>
|
|
||||||
</text>
|
|
||||||
</box>
|
|
||||||
<box
|
|
||||||
onMouseOver={() => setHover("next")}
|
|
||||||
onMouseOut={() => setHover(null)}
|
|
||||||
onMouseUp={() => keymap.dispatch("session.child.next")}
|
|
||||||
backgroundColor={hover() === "next" ? theme.background.action.primary.hovered : theme.background.default}
|
|
||||||
>
|
|
||||||
<text fg={theme.text.default}>
|
|
||||||
Next <span style={{ fg: theme.text.subdued }}>{shortcuts.get("session.child.next")}</span>
|
|
||||||
</text>
|
|
||||||
</box>
|
|
||||||
</box>
|
|
||||||
</box>
|
|
||||||
</box>
|
|
||||||
</box>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue