Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .husky/pre-push
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@ if (process.versions.bun !== expectedBunVersion) {
console.warn(`Warning: Bun version ${process.versions.bun} differs from expected ${expectedBunVersion}`);
}
'
bun typecheck
bun turbo typecheck --concurrency=3
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Limit Pre-Push Typecheck Concurrency Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Limit the repository's Husky pre-push typecheck to three parallel Turbo tasks.

**Architecture:** Modify only the pre-push hook to call the existing Turbo `typecheck` task graph with an explicit `--concurrency=3` argument. Keep the root package script and CI behavior unchanged.

**Tech Stack:** POSIX shell, Husky, Bun, Turborepo 2.8.13

## Global Constraints

- The concurrency value is exactly `3`.
- Only `.husky/pre-push` changes during implementation.
- The root `typecheck` package script remains unchanged.
- Validation must not execute the full typecheck workload.

---

### Task 1: Limit the pre-push typecheck

**Files:**
- Modify: `.husky/pre-push:20`
- Test: static shell assertions and Turbo dry-run

**Interfaces:**
- Consumes: the existing Turborepo `typecheck` task graph.
- Produces: a pre-push hook that schedules at most three Turbo tasks concurrently.

- [ ] **Step 1: Run the failing static assertion**

```bash
grep -Fx 'bun turbo typecheck --concurrency=3' .husky/pre-push
```

Expected: exit status `1` because the hook currently contains `bun typecheck`.

- [ ] **Step 2: Apply the minimal implementation**

Replace:

```sh
bun typecheck
```

with:

```sh
bun turbo typecheck --concurrency=3
```

- [ ] **Step 3: Run the passing static assertions**

```bash
grep -Fx 'bun turbo typecheck --concurrency=3' .husky/pre-push
! grep -Fx 'bun typecheck' .husky/pre-push
```

Expected: both assertions exit with status `0`.

- [ ] **Step 4: Validate shell syntax and Turbo argument parsing**

```bash
sh -n .husky/pre-push
bun turbo typecheck --concurrency=3 --dry=json >/tmp/opencode-typecheck-dry-run.json
jq -e '.tasks | type == "array"' /tmp/opencode-typecheck-dry-run.json
rm -f /tmp/opencode-typecheck-dry-run.json
```

Expected: all commands exit with status `0`; no `tsgo` typecheck workers are launched.

- [ ] **Step 5: Commit the hook change**

```bash
git add -- .husky/pre-push
git commit -m "fix: limit pre-push typecheck concurrency" -- .husky/pre-push
```

Expected: one commit containing only `.husky/pre-push`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Limit pre-push typecheck concurrency

## Problem

The repository's Husky `pre-push` hook runs `bun typecheck`, which expands to `bun turbo typecheck`. On the two-CPU, 3.8 GiB server, Turbo launched ten `tsgo` workers while checking 35 packages. Those workers exhausted RAM and swap and triggered the OOM killer.

## Decision

Change only `.husky/pre-push` to invoke Turbo directly with a concurrency limit:

```sh
bun turbo typecheck --concurrency=3
```

This keeps the existing typecheck task graph while allowing at most three parallel tasks during a push. The global `typecheck` package script remains unchanged, so local and CI callers retain their current behavior.

## Alternatives considered

- Add `--concurrency=3` to the root `typecheck` package script. Rejected because it changes every caller, not only the problematic push hook.
- Set `TURBO_CONCURRENCY=3` in the hook environment. Rejected because the effective limit is less visible than an explicit CLI argument.

## Validation

1. Before editing, verify the hook does not contain the required limited command.
2. After editing, verify the hook contains exactly `bun turbo typecheck --concurrency=3` and no unrestricted `bun typecheck` invocation.
3. Run the hook through `sh -n`.
4. Run Turbo with `--dry` and `--concurrency=3` to validate argument parsing without starting package typechecks.

## Out of scope

This change does not alter systemd services, OpenCode memory limits, CI behavior, or the root `typecheck` script.
1 change: 1 addition & 0 deletions packages/app/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,7 @@ export const dict = {
"session.messages.loadEarlier": "Load earlier messages",
"session.messages.loading": "Loading messages...",
"session.messages.jumpToLatest": "Jump to latest",
"session.messages.jumpToLatestUser": "Jump to last user message",

"session.context.addToContext": "Add {{selection}} to context",
"session.todo.title": "Todos",
Expand Down
1 change: 1 addition & 0 deletions packages/app/src/i18n/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,7 @@ export const dict = {
"session.messages.loadEarlier": "Загрузить предыдущие сообщения",
"session.messages.loading": "Загрузка сообщений...",
"session.messages.jumpToLatest": "Перейти к последнему",
"session.messages.jumpToLatestUser": "Перейти к последнему сообщению пользователя",

"session.context.addToContext": "Добавить {{selection}} в контекст",
"session.todo.title": "Задачи",
Expand Down
56 changes: 55 additions & 1 deletion packages/app/src/pages/session/timeline/message-timeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,14 @@ type TimelineRowByTag<T extends TimelineRow.TimelineRow["_tag"]> = Extract<Timel

const timelineFallbackItemSize = 60
const timelineCache = new Map<string, { measurements: VirtualItem[]; toolOpen: Record<string, boolean | undefined> }>()
const messageDisplayOptions = {
metadata: {
messageTimestamp: true,
toolTimestamp: true,
toolDuration: true,
toolStatus: true,
},
} as const

const taskDescription = (part: PartType, sessionID: string) => {
if (part.type !== "tool" || part.tool !== "task") return
Expand Down Expand Up @@ -464,6 +472,15 @@ export function MessageTimeline(props: {
() => new Map(virtualizer.getVirtualItems().map((item) => [item.key, item] as const)),
)
const virtualRowKeys = createMemo(() => virtualizer.getVirtualItems().map((item) => item.key as string))
const handleScrollToLastUser = () => {
const lastUser = props.userMessages.at(-1)
if (!lastUser) return
const index = messageRowIndex().get(lastUser.id)
if (index !== undefined) {
virtualizer.scrollToIndex(index, { align: "start" })
}
}

createEffect(() => {
props.setRevealMessage?.((id) => {
const index = messageRowIndex().get(id)
Expand Down Expand Up @@ -960,6 +977,7 @@ export function MessageTimeline(props: {
message={message()}
showAssistantCopyPartID={assistantCopyPartID(row().userMessageID)}
turnDurationMs={turnDurationMs(row().userMessageID)}
displayOptions={messageDisplayOptions}
defaultOpen={defaultOpen()}
toolOpen={toolOpen[part().id] ?? defaultOpen()}
onToolOpenChange={(open) => setToolOpen(part().id, open)}
Expand Down Expand Up @@ -1224,7 +1242,7 @@ export function MessageTimeline(props: {
return (
<div class="relative w-full h-full min-w-0">
<div
class="absolute left-1/2 -translate-x-1/2 z-[60] pointer-events-none transition-all duration-200 ease-out"
class="absolute left-1/2 -translate-x-1/2 z-[60] pointer-events-none transition-all duration-200 ease-out flex items-center gap-2"
classList={{
"bottom-8": settings.general.newLayoutDesigns(),
"bottom-6": !settings.general.newLayoutDesigns(),
Expand All @@ -1234,6 +1252,42 @@ export function MessageTimeline(props: {
"scale-95": (!props.scroll.overflow || !props.scroll.jump) && !settings.general.newLayoutDesigns(),
}}
>
<Show when={props.userMessages.length > 0}>
<Show
when={settings.general.newLayoutDesigns()}
fallback={
<button
type="button"
aria-label={language.t("session.messages.jumpToLatestUser")}
class="pointer-events-auto flex items-center justify-center w-10 h-8 bg-transparent border-none cursor-pointer p-0 group"
onClick={handleScrollToLastUser}
>
<div
class="flex items-center justify-center w-8 h-6 rounded-[6px] border border-border-weaker-base bg-[color-mix(in_srgb,var(--surface-raised-stronger-non-alpha)_80%,transparent)] backdrop-blur-[0.75px] transition-colors group-hover:border-[var(--border-weak-base)] group-hover:[--icon-base:var(--icon-hover)]"
style={{
"box-shadow":
"0 51px 60px 0 rgba(0,0,0,0.10), 0 15px 18px 0 rgba(0,0,0,0.12), 0 6.386px 7.513px 0 rgba(0,0,0,0.12), 0 2.31px 2.717px 0 rgba(0,0,0,0.20)",
}}
>
<Icon name="speech-bubble" size="small" />
</div>
</button>
}
>
<button
type="button"
aria-label={language.t("session.messages.jumpToLatestUser")}
class="pointer-events-auto flex items-center justify-center w-8 h-7 px-2 py-1.5 rounded-lg border-none cursor-pointer text-v2-text-text-base backdrop-blur-[2px]"
style={{
background: "color-mix(in srgb, var(--v2-background-bg-base) 92%, transparent)",
"box-shadow": "var(--v2-elevation-raised), 0px 2px 8px var(--v2-background-bg-base)",
}}
onClick={handleScrollToLastUser}
>
<Icon name="speech-bubble" size="small" />
</button>
</Show>
</Show>
<Show
when={settings.general.newLayoutDesigns()}
fallback={
Expand Down
1 change: 1 addition & 0 deletions packages/session-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"./*": "./src/components/*.tsx",
"./session-diff": "./src/components/session-diff.ts",
"./message-file": "./src/components/message-file.ts",
"./message-options": "./src/components/message-options.ts",
"./message-part-text": "./src/components/message-part-text.ts",
"./markdown-stream": "./src/components/markdown-stream.ts",
"./markdown-cache": "./src/components/markdown-cache.tsx",
Expand Down
16 changes: 15 additions & 1 deletion packages/session-ui/src/components/basic-tool.css
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,19 @@
color: var(--v2-text-text-muted);
}

[data-slot="basic-tool-tool-meta"] {
flex-shrink: 0;
margin-left: auto;
font-family: var(--font-family-sans);
font-variant-numeric: tabular-nums;
font-size: 12px;
font-style: normal;
font-weight: var(--font-weight-regular);
line-height: var(--line-height-small);
letter-spacing: var(--letter-spacing-normal);
color: var(--v2-text-text-weak);
}

[data-slot="basic-tool-tool-action"] {
display: inline-flex;
align-items: center;
Expand Down Expand Up @@ -260,7 +273,8 @@ body:not([data-new-layout]) {

[data-slot="basic-tool-tool-subtitle"],
[data-slot="basic-tool-tool-subtitle"].clickable:hover,
[data-slot="basic-tool-tool-arg"] {
[data-slot="basic-tool-tool-arg"],
[data-slot="basic-tool-tool-meta"] {
color: var(--text-base);
}
}
Expand Down
6 changes: 6 additions & 0 deletions packages/session-ui/src/components/basic-tool.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const isTriggerTitle = (val: any): val is TriggerTitle => {
export interface BasicToolProps {
icon: IconProps["name"]
trigger: TriggerTitle | JSX.Element | ((open: Accessor<boolean>) => JSX.Element)
meta?: string
children?: JSX.Element
status?: string
hideDetails?: boolean
Expand Down Expand Up @@ -244,6 +245,11 @@ export function BasicTool(props: BasicToolProps) {
<Match when={true}>{props.trigger as JSX.Element}</Match>
</Switch>
</div>
<Show when={props.meta}>
<span data-slot="basic-tool-tool-meta" class="text-12-regular text-text-weak cursor-default whitespace-nowrap">
{props.meta}
</span>
</Show>
</div>
<Show when={hasChildren() && !props.hideDetails && !props.locked && !pending()}>
<Collapsible.Arrow />
Expand Down
8 changes: 8 additions & 0 deletions packages/session-ui/src/components/message-actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export function handleCopyResponseClick(event: Pick<MouseEvent, "stopPropagation">, copy: () => void) {
event.stopPropagation()
copy()
}

export function formatMessageStamp(locale: string, value: number) {
return new Intl.DateTimeFormat(locale, { dateStyle: "medium", timeStyle: "short" }).format(value)
}
10 changes: 10 additions & 0 deletions packages/session-ui/src/components/message-options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export type MessageMetadataDisplayOptions = {
readonly messageTimestamp?: boolean
readonly toolTimestamp?: boolean
readonly toolDuration?: boolean
readonly toolStatus?: boolean
}

export type MessageDisplayOptions = {
readonly metadata?: MessageMetadataDisplayOptions
}
Loading
Loading