diff --git a/packages/pi-ghost-in-the-machine/ai-artifacts/docs/VISUAL_MODEL.md b/packages/pi-ghost-in-the-machine/ai-artifacts/docs/VISUAL_MODEL.md index f2a0725..1d1d31a 100644 --- a/packages/pi-ghost-in-the-machine/ai-artifacts/docs/VISUAL_MODEL.md +++ b/packages/pi-ghost-in-the-machine/ai-artifacts/docs/VISUAL_MODEL.md @@ -26,7 +26,7 @@ npm run generate ## 2. Placement -The face keeps one height-based size and one `16 px × 29` sidebar-center placement on every display. Its full animated footprint may move right only when needed to retain a `0.1%` viewport-width gap at the left edge; it never shrinks or switches layout based on cursor geometry. The face always renders on the dense `8 × 17 px` virtual ASCII grid. +The face keeps one height-based size on every display. Surfaces narrower than `3200 px` use the Mac placement: original `16 px × 29` center plus a `30 px` gap for the full animated footprint. Verified `3440 px` ultrawide surfaces use the fixed `144 px` center of Herdr's `36 columns × 8 px` sidebar. Placement deliberately ignores cursor geometry because cursor style and focus can invalidate it. The face always renders on the dense `8 × 17 px` virtual ASCII grid. ## 3. Visual invariants diff --git a/packages/pi-ghost-in-the-machine/scripts/sidebar-watcher.mjs b/packages/pi-ghost-in-the-machine/scripts/sidebar-watcher.mjs index fab72fc..af23e63 100755 --- a/packages/pi-ghost-in-the-machine/scripts/sidebar-watcher.mjs +++ b/packages/pi-ghost-in-the-machine/scripts/sidebar-watcher.mjs @@ -211,8 +211,8 @@ async function run(options) { let consecutiveFailures = 0; let totalLatencyMs = 0; let maxLatencyMs = 0; - let observedState; - let appliedState; + let observedTarget; + let appliedTarget; let nextActionAt = 0; let requestNumber = 0; let metricsStartedAt = performance.now(); @@ -270,12 +270,18 @@ async function run(options) { totalLatencyMs += latencyMs; maxLatencyMs = Math.max(maxLatencyMs, latencyMs); const state = layout.x <= 4 ? "collapsed" : "expanded"; - if (state !== observedState) { - observedState = state; + // Focus is part of the expanded target: switching Pi panes must restore the + // newly focused pane's remembered lifecycle state even when sidebar + // visibility does not change. Collapsed mode remains one deduplicated target. + const target = state === "expanded" + ? `${state}:${layout.focusedPaneId ?? "unknown"}` + : state; + if (target !== observedTarget) { + observedTarget = target; nextActionAt = 0; log(`state=${state} area_x=${layout.x} area_width=${layout.width ?? "unknown"} focused_pane_id=${JSON.stringify(layout.focusedPaneId ?? "unknown")} latency_ms=${latencyMs.toFixed(3)} socket=${JSON.stringify(options.socketPath)}`); } - if (state !== appliedState && Date.now() >= nextActionAt) { + if (target !== appliedTarget && Date.now() >= nextActionAt) { const action = await runController( options.controller, state, @@ -286,7 +292,7 @@ async function run(options) { (child) => { activeControllerChild = child; }, ); log(`action=ghost-${state} result=${action.ok ? "ok" : "error"} detail=${JSON.stringify(action.detail)}`); - if (action.ok) appliedState = state; + if (action.ok) appliedTarget = target; else nextActionAt = Date.now() + actionRetryMs; } } catch (error) { diff --git a/packages/pi-ghost-in-the-machine/shaders/ghost-in-the-machine.glsl b/packages/pi-ghost-in-the-machine/shaders/ghost-in-the-machine.glsl index 0e9aca2..74aff60 100644 --- a/packages/pi-ghost-in-the-machine/shaders/ghost-in-the-machine.glsl +++ b/packages/pi-ghost-in-the-machine/shaders/ghost-in-the-machine.glsl @@ -15,10 +15,12 @@ const vec2 CELL = vec2(8.0, 17.0); const float FACE_SIZE = 0.126; // 画面高に対する比(旧 0.14 から 10% 縮小) const float FACE_Y_FROM_TOP = 0.40; // 顔中心の上端からの位置(画面高比) -const float FACE_LEFT_GAP = 0.001; // 最も左の描画点と画面左端の最小距離(画面幅比) const float FACE_BOUND_X = 1.25; // drift/decorations 込みの顔空間左半径 -const float SIDEBAR_COLS = 29.0; // herdr 展開サイドバーの幅(列) -const float SIDEBAR_CELL_WIDTH = 16.0; // sidebar 配置用の実セル幅(dense grid とは独立) +const float ORIGINAL_SIDEBAR_COLS = 29.0; // Mac/original 配置の sidebar 幅(列) +const float ORIGINAL_CELL_WIDTH = 16.0; // Mac/original 配置の実セル幅 +const float MAC_FACE_LEFT_GAP = 30.0; // Mac の full footprint 左余白(px) +const float ULTRAWIDE_MIN_WIDTH = 3200.0; // 検証済み 3440px surface の識別境界 +const float ULTRAWIDE_CENTER_X = 144.0; // 36 columns × 8px の sidebar 中央 const vec3 FACE_COL = vec3(0.36, 0.88, 0.79); // 通常時の顔色(idle パレットアンカーも兼ねる) const vec3 ERR_COL = vec3(0.95, 0.36, 0.42); // 失敗時の顔色(err パレットアンカーも兼ねる) const vec3 THINK_COL = vec3(0.94, 0.76, 0.29); // 考える顔(調査/プランニング中) @@ -617,13 +619,18 @@ void mainImage(out vec4 fragColor, in vec2 fragCoord) { vec2 cellUV = fract(fragCoord / CELL); vec2 smp = (cellId + 0.5) * CELL; - // 元の高さ基準サイズと sidebar 中心配置を保つ。drift/decorations を含む - // 左端だけは viewport の 0.1% より内側へ入らないよう、必要な場合のみ右へ寄せる。 + // Ghostty 1.3.1 は display scale/sidebar geometry を直接公開しない。 + // cursor geometry は focus/style で消えるため display 判定には使わない。検証済み + // surfaces は MacBook <=3024px、ultrawide=3440px なので viewport 幅で安定分類する。 float faceScale = FACE_SIZE * iResolution.y; - float sidebarCenterX = SIDEBAR_CELL_WIDTH * SIDEBAR_COLS * 0.5; - float minCenterX = FACE_LEFT_GAP * iResolution.x + FACE_BOUND_X * faceScale; + bool ultrawideViewport = iResolution.x >= ULTRAWIDE_MIN_WIDTH; + float originalCenterX = ORIGINAL_CELL_WIDTH * ORIGINAL_SIDEBAR_COLS * 0.5; + float macMinCenterX = MAC_FACE_LEFT_GAP + FACE_BOUND_X * faceScale; + float faceCenterX = ultrawideViewport + ? ULTRAWIDE_CENTER_X + : max(originalCenterX, macMinCenterX); float centerYRatio = TOPDOWN_Y > 0.5 ? FACE_Y_FROM_TOP : 1.0 - FACE_Y_FROM_TOP; - vec2 faceCenter = vec2(max(sidebarCenterX, minCenterX), centerYRatio * iResolution.y); + vec2 faceCenter = vec2(faceCenterX, centerYRatio * iResolution.y); vec2 p = (smp - faceCenter) / faceScale; // TOPDOWN_Y=1(Ghostty): fragCoord.y は既に下向き正なので反転不要。 // TOPDOWN_Y=0(標準規約): fragCoord.y は上向き正なので反転して下向き正にする。 diff --git a/packages/pi-ghost-in-the-machine/shaders/variants/done.glsl b/packages/pi-ghost-in-the-machine/shaders/variants/done.glsl index 4e59cdf..e048f87 100644 --- a/packages/pi-ghost-in-the-machine/shaders/variants/done.glsl +++ b/packages/pi-ghost-in-the-machine/shaders/variants/done.glsl @@ -15,10 +15,12 @@ const vec2 CELL = vec2(8.0, 17.0); const float FACE_SIZE = 0.126; // 画面高に対する比(旧 0.14 から 10% 縮小) const float FACE_Y_FROM_TOP = 0.40; // 顔中心の上端からの位置(画面高比) -const float FACE_LEFT_GAP = 0.001; // 最も左の描画点と画面左端の最小距離(画面幅比) const float FACE_BOUND_X = 1.25; // drift/decorations 込みの顔空間左半径 -const float SIDEBAR_COLS = 29.0; // herdr 展開サイドバーの幅(列) -const float SIDEBAR_CELL_WIDTH = 16.0; // sidebar 配置用の実セル幅(dense grid とは独立) +const float ORIGINAL_SIDEBAR_COLS = 29.0; // Mac/original 配置の sidebar 幅(列) +const float ORIGINAL_CELL_WIDTH = 16.0; // Mac/original 配置の実セル幅 +const float MAC_FACE_LEFT_GAP = 30.0; // Mac の full footprint 左余白(px) +const float ULTRAWIDE_MIN_WIDTH = 3200.0; // 検証済み 3440px surface の識別境界 +const float ULTRAWIDE_CENTER_X = 144.0; // 36 columns × 8px の sidebar 中央 const vec3 FACE_COL = vec3(0.36, 0.88, 0.79); // 通常時の顔色(idle パレットアンカーも兼ねる) const vec3 ERR_COL = vec3(0.95, 0.36, 0.42); // 失敗時の顔色(err パレットアンカーも兼ねる) const vec3 THINK_COL = vec3(0.94, 0.76, 0.29); // 考える顔(調査/プランニング中) @@ -617,13 +619,18 @@ void mainImage(out vec4 fragColor, in vec2 fragCoord) { vec2 cellUV = fract(fragCoord / CELL); vec2 smp = (cellId + 0.5) * CELL; - // 元の高さ基準サイズと sidebar 中心配置を保つ。drift/decorations を含む - // 左端だけは viewport の 0.1% より内側へ入らないよう、必要な場合のみ右へ寄せる。 + // Ghostty 1.3.1 は display scale/sidebar geometry を直接公開しない。 + // cursor geometry は focus/style で消えるため display 判定には使わない。検証済み + // surfaces は MacBook <=3024px、ultrawide=3440px なので viewport 幅で安定分類する。 float faceScale = FACE_SIZE * iResolution.y; - float sidebarCenterX = SIDEBAR_CELL_WIDTH * SIDEBAR_COLS * 0.5; - float minCenterX = FACE_LEFT_GAP * iResolution.x + FACE_BOUND_X * faceScale; + bool ultrawideViewport = iResolution.x >= ULTRAWIDE_MIN_WIDTH; + float originalCenterX = ORIGINAL_CELL_WIDTH * ORIGINAL_SIDEBAR_COLS * 0.5; + float macMinCenterX = MAC_FACE_LEFT_GAP + FACE_BOUND_X * faceScale; + float faceCenterX = ultrawideViewport + ? ULTRAWIDE_CENTER_X + : max(originalCenterX, macMinCenterX); float centerYRatio = TOPDOWN_Y > 0.5 ? FACE_Y_FROM_TOP : 1.0 - FACE_Y_FROM_TOP; - vec2 faceCenter = vec2(max(sidebarCenterX, minCenterX), centerYRatio * iResolution.y); + vec2 faceCenter = vec2(faceCenterX, centerYRatio * iResolution.y); vec2 p = (smp - faceCenter) / faceScale; // TOPDOWN_Y=1(Ghostty): fragCoord.y は既に下向き正なので反転不要。 // TOPDOWN_Y=0(標準規約): fragCoord.y は上向き正なので反転して下向き正にする。 diff --git a/packages/pi-ghost-in-the-machine/shaders/variants/error.glsl b/packages/pi-ghost-in-the-machine/shaders/variants/error.glsl index 000b7a8..ca3aec7 100644 --- a/packages/pi-ghost-in-the-machine/shaders/variants/error.glsl +++ b/packages/pi-ghost-in-the-machine/shaders/variants/error.glsl @@ -15,10 +15,12 @@ const vec2 CELL = vec2(8.0, 17.0); const float FACE_SIZE = 0.126; // 画面高に対する比(旧 0.14 から 10% 縮小) const float FACE_Y_FROM_TOP = 0.40; // 顔中心の上端からの位置(画面高比) -const float FACE_LEFT_GAP = 0.001; // 最も左の描画点と画面左端の最小距離(画面幅比) const float FACE_BOUND_X = 1.25; // drift/decorations 込みの顔空間左半径 -const float SIDEBAR_COLS = 29.0; // herdr 展開サイドバーの幅(列) -const float SIDEBAR_CELL_WIDTH = 16.0; // sidebar 配置用の実セル幅(dense grid とは独立) +const float ORIGINAL_SIDEBAR_COLS = 29.0; // Mac/original 配置の sidebar 幅(列) +const float ORIGINAL_CELL_WIDTH = 16.0; // Mac/original 配置の実セル幅 +const float MAC_FACE_LEFT_GAP = 30.0; // Mac の full footprint 左余白(px) +const float ULTRAWIDE_MIN_WIDTH = 3200.0; // 検証済み 3440px surface の識別境界 +const float ULTRAWIDE_CENTER_X = 144.0; // 36 columns × 8px の sidebar 中央 const vec3 FACE_COL = vec3(0.36, 0.88, 0.79); // 通常時の顔色(idle パレットアンカーも兼ねる) const vec3 ERR_COL = vec3(0.95, 0.36, 0.42); // 失敗時の顔色(err パレットアンカーも兼ねる) const vec3 THINK_COL = vec3(0.94, 0.76, 0.29); // 考える顔(調査/プランニング中) @@ -617,13 +619,18 @@ void mainImage(out vec4 fragColor, in vec2 fragCoord) { vec2 cellUV = fract(fragCoord / CELL); vec2 smp = (cellId + 0.5) * CELL; - // 元の高さ基準サイズと sidebar 中心配置を保つ。drift/decorations を含む - // 左端だけは viewport の 0.1% より内側へ入らないよう、必要な場合のみ右へ寄せる。 + // Ghostty 1.3.1 は display scale/sidebar geometry を直接公開しない。 + // cursor geometry は focus/style で消えるため display 判定には使わない。検証済み + // surfaces は MacBook <=3024px、ultrawide=3440px なので viewport 幅で安定分類する。 float faceScale = FACE_SIZE * iResolution.y; - float sidebarCenterX = SIDEBAR_CELL_WIDTH * SIDEBAR_COLS * 0.5; - float minCenterX = FACE_LEFT_GAP * iResolution.x + FACE_BOUND_X * faceScale; + bool ultrawideViewport = iResolution.x >= ULTRAWIDE_MIN_WIDTH; + float originalCenterX = ORIGINAL_CELL_WIDTH * ORIGINAL_SIDEBAR_COLS * 0.5; + float macMinCenterX = MAC_FACE_LEFT_GAP + FACE_BOUND_X * faceScale; + float faceCenterX = ultrawideViewport + ? ULTRAWIDE_CENTER_X + : max(originalCenterX, macMinCenterX); float centerYRatio = TOPDOWN_Y > 0.5 ? FACE_Y_FROM_TOP : 1.0 - FACE_Y_FROM_TOP; - vec2 faceCenter = vec2(max(sidebarCenterX, minCenterX), centerYRatio * iResolution.y); + vec2 faceCenter = vec2(faceCenterX, centerYRatio * iResolution.y); vec2 p = (smp - faceCenter) / faceScale; // TOPDOWN_Y=1(Ghostty): fragCoord.y は既に下向き正なので反転不要。 // TOPDOWN_Y=0(標準規約): fragCoord.y は上向き正なので反転して下向き正にする。 diff --git a/packages/pi-ghost-in-the-machine/shaders/variants/idle.glsl b/packages/pi-ghost-in-the-machine/shaders/variants/idle.glsl index fd9a2ac..67538bc 100644 --- a/packages/pi-ghost-in-the-machine/shaders/variants/idle.glsl +++ b/packages/pi-ghost-in-the-machine/shaders/variants/idle.glsl @@ -15,10 +15,12 @@ const vec2 CELL = vec2(8.0, 17.0); const float FACE_SIZE = 0.126; // 画面高に対する比(旧 0.14 から 10% 縮小) const float FACE_Y_FROM_TOP = 0.40; // 顔中心の上端からの位置(画面高比) -const float FACE_LEFT_GAP = 0.001; // 最も左の描画点と画面左端の最小距離(画面幅比) const float FACE_BOUND_X = 1.25; // drift/decorations 込みの顔空間左半径 -const float SIDEBAR_COLS = 29.0; // herdr 展開サイドバーの幅(列) -const float SIDEBAR_CELL_WIDTH = 16.0; // sidebar 配置用の実セル幅(dense grid とは独立) +const float ORIGINAL_SIDEBAR_COLS = 29.0; // Mac/original 配置の sidebar 幅(列) +const float ORIGINAL_CELL_WIDTH = 16.0; // Mac/original 配置の実セル幅 +const float MAC_FACE_LEFT_GAP = 30.0; // Mac の full footprint 左余白(px) +const float ULTRAWIDE_MIN_WIDTH = 3200.0; // 検証済み 3440px surface の識別境界 +const float ULTRAWIDE_CENTER_X = 144.0; // 36 columns × 8px の sidebar 中央 const vec3 FACE_COL = vec3(0.36, 0.88, 0.79); // 通常時の顔色(idle パレットアンカーも兼ねる) const vec3 ERR_COL = vec3(0.95, 0.36, 0.42); // 失敗時の顔色(err パレットアンカーも兼ねる) const vec3 THINK_COL = vec3(0.94, 0.76, 0.29); // 考える顔(調査/プランニング中) @@ -617,13 +619,18 @@ void mainImage(out vec4 fragColor, in vec2 fragCoord) { vec2 cellUV = fract(fragCoord / CELL); vec2 smp = (cellId + 0.5) * CELL; - // 元の高さ基準サイズと sidebar 中心配置を保つ。drift/decorations を含む - // 左端だけは viewport の 0.1% より内側へ入らないよう、必要な場合のみ右へ寄せる。 + // Ghostty 1.3.1 は display scale/sidebar geometry を直接公開しない。 + // cursor geometry は focus/style で消えるため display 判定には使わない。検証済み + // surfaces は MacBook <=3024px、ultrawide=3440px なので viewport 幅で安定分類する。 float faceScale = FACE_SIZE * iResolution.y; - float sidebarCenterX = SIDEBAR_CELL_WIDTH * SIDEBAR_COLS * 0.5; - float minCenterX = FACE_LEFT_GAP * iResolution.x + FACE_BOUND_X * faceScale; + bool ultrawideViewport = iResolution.x >= ULTRAWIDE_MIN_WIDTH; + float originalCenterX = ORIGINAL_CELL_WIDTH * ORIGINAL_SIDEBAR_COLS * 0.5; + float macMinCenterX = MAC_FACE_LEFT_GAP + FACE_BOUND_X * faceScale; + float faceCenterX = ultrawideViewport + ? ULTRAWIDE_CENTER_X + : max(originalCenterX, macMinCenterX); float centerYRatio = TOPDOWN_Y > 0.5 ? FACE_Y_FROM_TOP : 1.0 - FACE_Y_FROM_TOP; - vec2 faceCenter = vec2(max(sidebarCenterX, minCenterX), centerYRatio * iResolution.y); + vec2 faceCenter = vec2(faceCenterX, centerYRatio * iResolution.y); vec2 p = (smp - faceCenter) / faceScale; // TOPDOWN_Y=1(Ghostty): fragCoord.y は既に下向き正なので反転不要。 // TOPDOWN_Y=0(標準規約): fragCoord.y は上向き正なので反転して下向き正にする。 diff --git a/packages/pi-ghost-in-the-machine/shaders/variants/thinking.glsl b/packages/pi-ghost-in-the-machine/shaders/variants/thinking.glsl index 5b42f3c..6f0011e 100644 --- a/packages/pi-ghost-in-the-machine/shaders/variants/thinking.glsl +++ b/packages/pi-ghost-in-the-machine/shaders/variants/thinking.glsl @@ -15,10 +15,12 @@ const vec2 CELL = vec2(8.0, 17.0); const float FACE_SIZE = 0.126; // 画面高に対する比(旧 0.14 から 10% 縮小) const float FACE_Y_FROM_TOP = 0.40; // 顔中心の上端からの位置(画面高比) -const float FACE_LEFT_GAP = 0.001; // 最も左の描画点と画面左端の最小距離(画面幅比) const float FACE_BOUND_X = 1.25; // drift/decorations 込みの顔空間左半径 -const float SIDEBAR_COLS = 29.0; // herdr 展開サイドバーの幅(列) -const float SIDEBAR_CELL_WIDTH = 16.0; // sidebar 配置用の実セル幅(dense grid とは独立) +const float ORIGINAL_SIDEBAR_COLS = 29.0; // Mac/original 配置の sidebar 幅(列) +const float ORIGINAL_CELL_WIDTH = 16.0; // Mac/original 配置の実セル幅 +const float MAC_FACE_LEFT_GAP = 30.0; // Mac の full footprint 左余白(px) +const float ULTRAWIDE_MIN_WIDTH = 3200.0; // 検証済み 3440px surface の識別境界 +const float ULTRAWIDE_CENTER_X = 144.0; // 36 columns × 8px の sidebar 中央 const vec3 FACE_COL = vec3(0.36, 0.88, 0.79); // 通常時の顔色(idle パレットアンカーも兼ねる) const vec3 ERR_COL = vec3(0.95, 0.36, 0.42); // 失敗時の顔色(err パレットアンカーも兼ねる) const vec3 THINK_COL = vec3(0.94, 0.76, 0.29); // 考える顔(調査/プランニング中) @@ -617,13 +619,18 @@ void mainImage(out vec4 fragColor, in vec2 fragCoord) { vec2 cellUV = fract(fragCoord / CELL); vec2 smp = (cellId + 0.5) * CELL; - // 元の高さ基準サイズと sidebar 中心配置を保つ。drift/decorations を含む - // 左端だけは viewport の 0.1% より内側へ入らないよう、必要な場合のみ右へ寄せる。 + // Ghostty 1.3.1 は display scale/sidebar geometry を直接公開しない。 + // cursor geometry は focus/style で消えるため display 判定には使わない。検証済み + // surfaces は MacBook <=3024px、ultrawide=3440px なので viewport 幅で安定分類する。 float faceScale = FACE_SIZE * iResolution.y; - float sidebarCenterX = SIDEBAR_CELL_WIDTH * SIDEBAR_COLS * 0.5; - float minCenterX = FACE_LEFT_GAP * iResolution.x + FACE_BOUND_X * faceScale; + bool ultrawideViewport = iResolution.x >= ULTRAWIDE_MIN_WIDTH; + float originalCenterX = ORIGINAL_CELL_WIDTH * ORIGINAL_SIDEBAR_COLS * 0.5; + float macMinCenterX = MAC_FACE_LEFT_GAP + FACE_BOUND_X * faceScale; + float faceCenterX = ultrawideViewport + ? ULTRAWIDE_CENTER_X + : max(originalCenterX, macMinCenterX); float centerYRatio = TOPDOWN_Y > 0.5 ? FACE_Y_FROM_TOP : 1.0 - FACE_Y_FROM_TOP; - vec2 faceCenter = vec2(max(sidebarCenterX, minCenterX), centerYRatio * iResolution.y); + vec2 faceCenter = vec2(faceCenterX, centerYRatio * iResolution.y); vec2 p = (smp - faceCenter) / faceScale; // TOPDOWN_Y=1(Ghostty): fragCoord.y は既に下向き正なので反転不要。 // TOPDOWN_Y=0(標準規約): fragCoord.y は上向き正なので反転して下向き正にする。 diff --git a/packages/pi-ghost-in-the-machine/shaders/variants/working.glsl b/packages/pi-ghost-in-the-machine/shaders/variants/working.glsl index f0b8926..49a1536 100644 --- a/packages/pi-ghost-in-the-machine/shaders/variants/working.glsl +++ b/packages/pi-ghost-in-the-machine/shaders/variants/working.glsl @@ -15,10 +15,12 @@ const vec2 CELL = vec2(8.0, 17.0); const float FACE_SIZE = 0.126; // 画面高に対する比(旧 0.14 から 10% 縮小) const float FACE_Y_FROM_TOP = 0.40; // 顔中心の上端からの位置(画面高比) -const float FACE_LEFT_GAP = 0.001; // 最も左の描画点と画面左端の最小距離(画面幅比) const float FACE_BOUND_X = 1.25; // drift/decorations 込みの顔空間左半径 -const float SIDEBAR_COLS = 29.0; // herdr 展開サイドバーの幅(列) -const float SIDEBAR_CELL_WIDTH = 16.0; // sidebar 配置用の実セル幅(dense grid とは独立) +const float ORIGINAL_SIDEBAR_COLS = 29.0; // Mac/original 配置の sidebar 幅(列) +const float ORIGINAL_CELL_WIDTH = 16.0; // Mac/original 配置の実セル幅 +const float MAC_FACE_LEFT_GAP = 30.0; // Mac の full footprint 左余白(px) +const float ULTRAWIDE_MIN_WIDTH = 3200.0; // 検証済み 3440px surface の識別境界 +const float ULTRAWIDE_CENTER_X = 144.0; // 36 columns × 8px の sidebar 中央 const vec3 FACE_COL = vec3(0.36, 0.88, 0.79); // 通常時の顔色(idle パレットアンカーも兼ねる) const vec3 ERR_COL = vec3(0.95, 0.36, 0.42); // 失敗時の顔色(err パレットアンカーも兼ねる) const vec3 THINK_COL = vec3(0.94, 0.76, 0.29); // 考える顔(調査/プランニング中) @@ -617,13 +619,18 @@ void mainImage(out vec4 fragColor, in vec2 fragCoord) { vec2 cellUV = fract(fragCoord / CELL); vec2 smp = (cellId + 0.5) * CELL; - // 元の高さ基準サイズと sidebar 中心配置を保つ。drift/decorations を含む - // 左端だけは viewport の 0.1% より内側へ入らないよう、必要な場合のみ右へ寄せる。 + // Ghostty 1.3.1 は display scale/sidebar geometry を直接公開しない。 + // cursor geometry は focus/style で消えるため display 判定には使わない。検証済み + // surfaces は MacBook <=3024px、ultrawide=3440px なので viewport 幅で安定分類する。 float faceScale = FACE_SIZE * iResolution.y; - float sidebarCenterX = SIDEBAR_CELL_WIDTH * SIDEBAR_COLS * 0.5; - float minCenterX = FACE_LEFT_GAP * iResolution.x + FACE_BOUND_X * faceScale; + bool ultrawideViewport = iResolution.x >= ULTRAWIDE_MIN_WIDTH; + float originalCenterX = ORIGINAL_CELL_WIDTH * ORIGINAL_SIDEBAR_COLS * 0.5; + float macMinCenterX = MAC_FACE_LEFT_GAP + FACE_BOUND_X * faceScale; + float faceCenterX = ultrawideViewport + ? ULTRAWIDE_CENTER_X + : max(originalCenterX, macMinCenterX); float centerYRatio = TOPDOWN_Y > 0.5 ? FACE_Y_FROM_TOP : 1.0 - FACE_Y_FROM_TOP; - vec2 faceCenter = vec2(max(sidebarCenterX, minCenterX), centerYRatio * iResolution.y); + vec2 faceCenter = vec2(faceCenterX, centerYRatio * iResolution.y); vec2 p = (smp - faceCenter) / faceScale; // TOPDOWN_Y=1(Ghostty): fragCoord.y は既に下向き正なので反転不要。 // TOPDOWN_Y=0(標準規約): fragCoord.y は上向き正なので反転して下向き正にする。 diff --git a/packages/pi-ghost-in-the-machine/tests/sidebar-watcher.test.ts b/packages/pi-ghost-in-the-machine/tests/sidebar-watcher.test.ts index ae64b72..2637e6b 100644 --- a/packages/pi-ghost-in-the-machine/tests/sidebar-watcher.test.ts +++ b/packages/pi-ghost-in-the-machine/tests/sidebar-watcher.test.ts @@ -200,7 +200,7 @@ function watcherEnvironment( }; } -test("watcher frames pane.layout requests, serializes polls, and deduplicates transitions", async () => { +test("watcher frames requests and reacts to visibility or focus transitions", async () => { const directory = temporaryDirectory("gitm protocol "); const stateHome = join(directory, "state with spaces"); mkdirSync(stateHome, { recursive: true }); @@ -208,6 +208,7 @@ test("watcher frames pane.layout requests, serializes polls, and deduplicates tr const herdr = await startFakeHerdr(directory, [ { kind: "layout", x: 36, width: 82, focusedPaneId: "pane-1", delayMs: 30 }, { kind: "layout", x: 36, width: 82, focusedPaneId: "pane-1" }, + { kind: "layout", x: 36, width: 82, focusedPaneId: "pane-2" }, { kind: "layout", x: 4, width: 114 }, { kind: "malformed" }, { kind: "error" }, @@ -232,10 +233,10 @@ test("watcher frames pane.layout requests, serializes polls, and deduplicates tr }); try { - await waitFor(() => herdr.requests.length >= 8, "watcher did not complete scripted requests"); + await waitFor(() => herdr.requests.length >= 9, "watcher did not complete scripted requests"); await waitFor( - () => existsSync(fakeController.log) && readFileSync(fakeController.log, "utf8").split("\n").filter(Boolean).length >= 3, - "watcher did not apply the final expanded transition", + () => existsSync(fakeController.log) && readFileSync(fakeController.log, "utf8").split("\n").filter(Boolean).length >= 4, + "watcher did not apply visibility and focus transitions", ); child.kill("SIGTERM"); await waitForExit(child); @@ -244,7 +245,10 @@ test("watcher frames pane.layout requests, serializes polls, and deduplicates tr assert.ok(herdr.requests.every((request) => request.method === "pane.layout")); assert.ok(herdr.requests.every((request) => JSON.stringify(request.params) === "{}")); assert.equal(new Set(herdr.requests.map((request) => request.id)).size, herdr.requests.length); - assert.equal(readFileSync(fakeController.log, "utf8"), "expanded:pane-1\ncollapsed\nexpanded:pane-1\n"); + assert.equal( + readFileSync(fakeController.log, "utf8"), + "expanded:pane-1\nexpanded:pane-2\ncollapsed\nexpanded:pane-1\n", + ); assert.equal(readFileSync(socketFile, "utf8"), `${canonicalSocketPath(herdr.socketPath)}\n`); assert.equal(existsSync(pidFile), false);