Skip to content
Merged
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
101 changes: 101 additions & 0 deletions .claude/skills/upstream-sync/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
---
name: upstream-sync
description: "Verifica atualizacoes no upstream do t3code e compara com o changelog local. Use quando o usuario pedir '/upstream-sync', 'verifica upstream', 'check upstream', 'atualiza upstream', ou qualquer pedido para verificar novidades do repositorio upstream."
argument-hint: ""
allowed-tools: Bash, Read, Write, Edit, Glob, Grep, Agent
---

# Upstream Sync — Verificacao e analise de atualizacoes

## Workflow

### 1. Ler o changelog local

Leia o arquivo `.context/upstream-sync.md` no diretorio do workspace.

- Extraia o **ultimo commit upstream sincronizado** (hash e data)
- Extraia a lista de **mudancas locais exclusivas** (reimplementacoes)
- Se o arquivo nao existir, avise o usuario e faca a analise completa

### 2. Fetch upstream

```bash
git fetch upstream
```

### 3. Listar novos commits

Liste apenas commits do upstream **posteriores** ao ultimo commit sincronizado:

```bash
git log <ultimo_hash_sincronizado>..upstream/main --oneline --reverse
```

Se retornar vazio, informe: "Nenhuma atualizacao nova no upstream desde o ultimo sync."

### 4. Analise cruzada

Para cada novo commit upstream:

1. **Verificar por PR number**: buscar `git log origin/main --oneline --grep="#NNN"`
2. **Verificar por conteudo semantico**: comparar a descricao com a tabela de "Mudancas locais exclusivas" do changelog
3. **Verificar por arquivos modificados**: `git show <hash> --stat` e comparar se os mesmos arquivos foram alterados localmente

Classificar cada commit em:

- **Ja sincronizado** — existe localmente (por hash, PR, ou reimplementacao)
- **Novo simples** — mudanca isolada sem conflito previsto (CSS, docs, config, chore)
- **Novo moderado** — feature/fix que toca areas comuns mas sem sobreposicao direta
- **Atencao especial** — toca areas que foram modificadas significativamente no fork (ChatView, sub-threads, skills, streaming, etc.)

### 5. Apresentar relatorio

Formato:

```
## Upstream Sync Report — [data]

### Resumo
- X commits novos no upstream
- Y ja sincronizados
- Z pendentes (N simples, M moderados, K atencao especial)

### Pendentes — Simples
| Hash | Descricao | Arquivos |
|---|---|---|

### Pendentes — Moderado
| Hash | Descricao | Arquivos | Risco |
|---|---|---|---|

### Pendentes — Atencao Especial
| Hash | Descricao | Arquivos | Conflito potencial |
|---|---|---|---|

### Ja sincronizados (ignorados)
<lista resumida>
```

### 6. Perguntar ao usuario

Apos o relatorio, perguntar:

- "Quer que eu traga os commits simples agora?"
- "Quer revisar os moderados/especiais individualmente?"

### 7. Atualizar changelog

Apos qualquer sync realizado, atualizar `.context/upstream-sync.md`:

- Atualizar "Ultimo sync" com a nova data
- Atualizar "Ultimo commit upstream sincronizado"
- Adicionar entrada no historico
- Atualizar tabela de "Mudancas locais exclusivas" se necessario

## Regras importantes

- NUNCA assumir que um commit upstream ja foi trazido apenas por similaridade vaga. Verificar arquivos e conteudo real.
- Commits de contribuidores (vouched lists, typos em docs) sao sempre "simples"
- Commits que tocam `ChatView.tsx`, `chat/`, `composer/`, `skills/`, `streaming/` precisam de verificacao extra contra mudancas locais
- Sempre preservar features exclusivas do fork (sub-threads, skills, Claude adapter, favorite model)
- Preferir cherry-pick individual para commits simples e squash merge para lotes grandes
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ build/
.logs/
release/
.t3
.idea/
apps/web/.playwright
apps/web/playwright-report
apps/web/src/components/__screenshots__
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ If a tradeoff is required, choose correctness and robustness over short-term con

## Maintainability

Long term maintainability is a core priority. If you add new functionality, first check if there are shared logic that can be extracted to a separate module. Duplicate logic across mulitple files is a code smell and should be avoided. Don't be afraid to change existing code. Don't take shortcuts by just adding local logic to solve a problem.
Long term maintainability is a core priority. If you add new functionality, first check if there is shared logic that can be extracted to a separate module. Duplicate logic across multiple files is a code smell and should be avoided. Don't be afraid to change existing code. Don't take shortcuts by just adding local logic to solve a problem.

## Package Roles

Expand Down
2 changes: 1 addition & 1 deletion apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,6 @@
"vitest": "catalog:"
},
"engines": {
"node": "^22.13 || ^23.4 || >=24.10"
"node": "^22.16 || ^23.11 || >=24.10"
}
}
24 changes: 24 additions & 0 deletions apps/server/src/persistence/NodeSqliteClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,35 @@ export interface SqliteMemoryClientConfig extends Omit<
"filename" | "readonly"
> {}

/**
* Verify that the current Node.js version includes the `node:sqlite` APIs
* used by `NodeSqliteClient` — specifically `StatementSync.columns()` (added
* in Node 22.16.0 / 23.11.0).
*
* @see https://github.com/nodejs/node/pull/57490
*/
const checkNodeSqliteCompat = () => {
const parts = process.versions.node.split(".").map(Number);
const major = parts[0] ?? 0;
const minor = parts[1] ?? 0;
const supported = (major === 22 && minor >= 16) || (major === 23 && minor >= 11) || major >= 24;

if (!supported) {
return Effect.die(
`Node.js ${process.versions.node} is missing required node:sqlite APIs ` +
`(StatementSync.columns). Upgrade to Node.js >=22.16, >=23.11, or >=24.`,
);
}
return Effect.void;
};

const makeWithDatabase = (
options: SqliteClientConfig,
openDatabase: () => DatabaseSync,
): Effect.Effect<Client.SqlClient, never, Scope.Scope | Reactivity.Reactivity> =>
Effect.gen(function* () {
yield* checkNodeSqliteCompat();

const compiler = Statement.makeCompilerSqlite(options.transformQueryNames);
const transformRows = options.transformResultNames
? Statement.defaultTransforms(options.transformResultNames).array
Expand Down
155 changes: 87 additions & 68 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@
(store) => store.draftThreadsByThreadId[threadId] ?? null,
);
const promptRef = useRef(prompt);
const [showScrollToBottom, setShowScrollToBottom] = useState(false);
const [isDragOverComposer, setIsDragOverComposer] = useState(false);
const [expandedImage, setExpandedImage] = useState<ExpandedImagePreview | null>(null);
const [optimisticUserMessages, setOptimisticUserMessages] = useState<ChatMessage[]>([]);
Expand Down Expand Up @@ -1709,6 +1710,7 @@
}
}

setShowScrollToBottom(!shouldAutoScrollRef.current);
lastKnownScrollTopRef.current = currentScrollTop;
}, []);
const onMessagesWheel = useCallback((event: React.WheelEvent<HTMLDivElement>) => {
Expand Down Expand Up @@ -3150,7 +3152,7 @@
const nextThreadModel: ModelSlug =
selectedModel ||
(activeSubThread?.model as ModelSlug) ||
(activeProject.model as ModelSlug) ||

Check warning on line 3155 in apps/web/src/components/ChatView.tsx

View workflow job for this annotation

GitHub Actions / Format, Lint, Typecheck, Test, Browser Test, Build

eslint-plugin-react-hooks(exhaustive-deps)

React Hook useCallback has a missing dependency: 'activeSubThread.model'
(globalFavorite?.model as ModelSlug) ||
DEFAULT_MODEL_BY_PROVIDER.codex;

Expand Down Expand Up @@ -3635,79 +3637,96 @@
<div className="flex min-h-0 min-w-0 flex-1">
{/* Chat column */}
<div className="flex min-h-0 min-w-0 flex-1 flex-col">
{/* Messages */}
<div
ref={setMessagesScrollContainerRef}
className="min-h-0 flex-1 overflow-x-hidden overflow-y-auto overscroll-y-contain px-3 py-3 sm:px-5 sm:py-4"
onScroll={onMessagesScroll}
onClickCapture={onMessagesClickCapture}
onWheel={onMessagesWheel}
onPointerDown={onMessagesPointerDown}
onPointerUp={onMessagesPointerUp}
onPointerCancel={onMessagesPointerCancel}
onTouchStart={onMessagesTouchStart}
onTouchMove={onMessagesTouchMove}
onTouchEnd={onMessagesTouchEnd}
onTouchCancel={onMessagesTouchEnd}
>
{activeThread.implementationThreadId && (
<div className="mb-3 flex items-center gap-2 rounded-lg border border-emerald-500/20 bg-emerald-500/5 px-3 py-2 text-xs text-emerald-700 dark:text-emerald-300/90">
<span>Plan implemented in another thread.</span>
<button
type="button"
className="font-medium underline underline-offset-2 hover:text-emerald-900 dark:hover:text-emerald-100"
onClick={() => {
void navigate({
to: "/$threadId",
params: { threadId: activeThread.implementationThreadId! },
});
}}
>
Go to implementation
</button>
</div>
)}
{activeThread.sourceThreadId && (
<div className="mb-3 flex items-center gap-2 rounded-lg border border-violet-500/20 bg-violet-500/5 px-3 py-2 text-xs text-violet-700 dark:text-violet-300/90">
<span>Implements plan from another thread.</span>
{/* Messages Wrapper */}
<div className="relative flex min-h-0 flex-1 flex-col">
{/* Messages */}
<div
ref={setMessagesScrollContainerRef}
className="min-h-0 flex-1 overflow-x-hidden overflow-y-auto overscroll-y-contain px-3 py-3 sm:px-5 sm:py-4"
onScroll={onMessagesScroll}
onClickCapture={onMessagesClickCapture}
onWheel={onMessagesWheel}
onPointerDown={onMessagesPointerDown}
onPointerUp={onMessagesPointerUp}
onPointerCancel={onMessagesPointerCancel}
onTouchStart={onMessagesTouchStart}
onTouchMove={onMessagesTouchMove}
onTouchEnd={onMessagesTouchEnd}
onTouchCancel={onMessagesTouchEnd}
>
{activeThread.implementationThreadId && (
<div className="mb-3 flex items-center gap-2 rounded-lg border border-emerald-500/20 bg-emerald-500/5 px-3 py-2 text-xs text-emerald-700 dark:text-emerald-300/90">
<span>Plan implemented in another thread.</span>
<button
type="button"
className="font-medium underline underline-offset-2 hover:text-emerald-900 dark:hover:text-emerald-100"
onClick={() => {
void navigate({
to: "/$threadId",
params: { threadId: activeThread.implementationThreadId! },
});
}}
>
Go to implementation
</button>
</div>
)}
{activeThread.sourceThreadId && (
<div className="mb-3 flex items-center gap-2 rounded-lg border border-violet-500/20 bg-violet-500/5 px-3 py-2 text-xs text-violet-700 dark:text-violet-300/90">
<span>Implements plan from another thread.</span>
<button
type="button"
className="font-medium underline underline-offset-2 hover:text-violet-900 dark:hover:text-violet-100"
onClick={() => {
void navigate({
to: "/$threadId",
params: { threadId: activeThread.sourceThreadId! },
});
}}
>
View plan
</button>
</div>
)}
<MessagesTimeline
key={activeThread.id}
hasMessages={timelineEntries.length > 0}
isWorking={isWorking}
activeTurnInProgress={isWorking || !latestTurnSettled}
activeTurnStartedAt={activeWorkStartedAt}
scrollContainer={messagesScrollElement}
timelineEntries={timelineEntries}
completionDividerBeforeEntryId={completionDividerBeforeEntryId}
completionSummary={completionSummary}
turnDiffSummaryByAssistantMessageId={turnDiffSummaryByAssistantMessageId}
nowIso={nowIso}
expandedWorkGroups={expandedWorkGroups}
onToggleWorkGroup={onToggleWorkGroup}
onOpenTurnDiff={onOpenTurnDiff}
revertTurnCountByUserMessageId={revertTurnCountByUserMessageId}
onRevertUserMessage={onRevertUserMessage}
isRevertingCheckpoint={isRevertingCheckpoint}
onImageExpand={onExpandTimelineImage}
markdownCwd={gitCwd ?? undefined}
resolvedTheme={resolvedTheme}
timestampFormat={timestampFormat}
workspaceRoot={activeProject?.cwd ?? undefined}
/>
</div>

{/* scroll to bottom pill — shown when user has scrolled away from the bottom */}
{showScrollToBottom && (
<div className="pointer-events-none absolute bottom-1 left-1/2 z-30 flex -translate-x-1/2 justify-center py-1.5">
<button
type="button"
className="font-medium underline underline-offset-2 hover:text-violet-900 dark:hover:text-violet-100"
onClick={() => {
void navigate({
to: "/$threadId",
params: { threadId: activeThread.sourceThreadId! },
});
}}
onClick={() => scrollMessagesToBottom("smooth")}
className="pointer-events-auto flex items-center gap-1.5 rounded-full border border-border/60 bg-card px-3 py-1 text-muted-foreground text-xs shadow-sm transition-colors hover:bg-accent hover:text-accent-foreground"
>
View plan
<ChevronDownIcon className="size-3.5" />
Scroll to bottom
</button>
</div>
)}
<MessagesTimeline
key={activeThread.id}
hasMessages={timelineEntries.length > 0}
isWorking={isWorking}
activeTurnInProgress={isWorking || !latestTurnSettled}
activeTurnStartedAt={activeWorkStartedAt}
scrollContainer={messagesScrollElement}
timelineEntries={timelineEntries}
completionDividerBeforeEntryId={completionDividerBeforeEntryId}
completionSummary={completionSummary}
turnDiffSummaryByAssistantMessageId={turnDiffSummaryByAssistantMessageId}
nowIso={nowIso}
expandedWorkGroups={expandedWorkGroups}
onToggleWorkGroup={onToggleWorkGroup}
onOpenTurnDiff={onOpenTurnDiff}
revertTurnCountByUserMessageId={revertTurnCountByUserMessageId}
onRevertUserMessage={onRevertUserMessage}
isRevertingCheckpoint={isRevertingCheckpoint}
onImageExpand={onExpandTimelineImage}
markdownCwd={gitCwd ?? undefined}
resolvedTheme={resolvedTheme}
timestampFormat={timestampFormat}
workspaceRoot={activeProject?.cwd ?? undefined}
/>
</div>

{/* Input bar */}
Expand Down Expand Up @@ -3738,7 +3757,7 @@
<div className="rounded-t-[19px] border-b border-border/65 bg-muted/20">
<ComposerPendingUserInputPanel
pendingUserInputs={pendingUserInputs}
respondingRequestIds={respondingUserInputRequestIds}
respondingRequestIds={respondingRequestIds}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use user-input response state for pending input panel

Pass the user-input in-flight IDs here instead of approval in-flight IDs. ComposerPendingUserInputPanel uses respondingRequestIds to disable options while thread.user-input.respond is pending, but this prop now receives respondingRequestIds (approval state), so user-input prompts stay interactive during submission and can trigger duplicate responses; additionally, an unrelated approval request can incorrectly mark a user-input prompt as responding.

Useful? React with 👍 / 👎.

answers={activePendingDraftAnswers}
questionIndex={activePendingQuestionIndex}
onSelectOption={onSelectActivePendingUserInputOption}
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/Sidebar.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export function resolveThreadRowClassName(input: {
isSelected: boolean;
}): string {
const baseClassName =
"h-7 w-full translate-x-0 cursor-default justify-start px-2 text-left select-none focus-visible:ring-0";
"h-7 w-full translate-x-0 cursor-default justify-start px-2 text-left select-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-ring";

if (input.isSelected && input.isActive) {
return cn(
Expand Down
Loading
Loading