diff --git a/.claude/skills/upstream-sync/SKILL.md b/.claude/skills/upstream-sync/SKILL.md new file mode 100644 index 0000000000..5013f9b86f --- /dev/null +++ b/.claude/skills/upstream-sync/SKILL.md @@ -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 ..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 --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) + +``` + +### 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 diff --git a/.gitignore b/.gitignore index 8478eb6fc6..c8a1c30805 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ build/ .logs/ release/ .t3 +.idea/ apps/web/.playwright apps/web/playwright-report apps/web/src/components/__screenshots__ diff --git a/AGENTS.md b/AGENTS.md index af81aa0bd0..dccffe2063 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 diff --git a/apps/server/package.json b/apps/server/package.json index 2a44080435..1b8418ef17 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -45,6 +45,6 @@ "vitest": "catalog:" }, "engines": { - "node": "^22.13 || ^23.4 || >=24.10" + "node": "^22.16 || ^23.11 || >=24.10" } } diff --git a/apps/server/src/persistence/NodeSqliteClient.ts b/apps/server/src/persistence/NodeSqliteClient.ts index b121764fcc..1d6e22d9b0 100644 --- a/apps/server/src/persistence/NodeSqliteClient.ts +++ b/apps/server/src/persistence/NodeSqliteClient.ts @@ -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 => Effect.gen(function* () { + yield* checkNodeSqliteCompat(); + const compiler = Statement.makeCompilerSqlite(options.transformQueryNames); const transformRows = options.transformResultNames ? Statement.defaultTransforms(options.transformResultNames).array diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index d751435287..0e0bbcdffd 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -265,6 +265,7 @@ export default function ChatView({ threadId }: ChatViewProps) { (store) => store.draftThreadsByThreadId[threadId] ?? null, ); const promptRef = useRef(prompt); + const [showScrollToBottom, setShowScrollToBottom] = useState(false); const [isDragOverComposer, setIsDragOverComposer] = useState(false); const [expandedImage, setExpandedImage] = useState(null); const [optimisticUserMessages, setOptimisticUserMessages] = useState([]); @@ -1709,6 +1710,7 @@ export default function ChatView({ threadId }: ChatViewProps) { } } + setShowScrollToBottom(!shouldAutoScrollRef.current); lastKnownScrollTopRef.current = currentScrollTop; }, []); const onMessagesWheel = useCallback((event: React.WheelEvent) => { @@ -3635,79 +3637,96 @@ export default function ChatView({ threadId }: ChatViewProps) {
{/* Chat column */}
- {/* Messages */} -
- {activeThread.implementationThreadId && ( -
- Plan implemented in another thread. - -
- )} - {activeThread.sourceThreadId && ( -
- Implements plan from another thread. + {/* Messages Wrapper */} +
+ {/* Messages */} +
+ {activeThread.implementationThreadId && ( +
+ Plan implemented in another thread. + +
+ )} + {activeThread.sourceThreadId && ( +
+ Implements plan from another thread. + +
+ )} + 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} + /> +
+ + {/* scroll to bottom pill — shown when user has scrolled away from the bottom */} + {showScrollToBottom && ( +
)} - 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} - />
{/* Input bar */} @@ -3738,7 +3757,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
) { function SidebarGroupLabel({ className, render, ...props }: useRender.ComponentProps<"div">) { const defaultProps = { className: cn( - "flex h-8 shrink-0 items-center rounded-lg px-2 font-medium text-sidebar-foreground text-xs outline-hidden ring-sidebar-ring transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0", + "flex h-8 shrink-0 items-center rounded-lg px-2 font-medium text-sidebar-foreground text-xs outline-hidden ring-ring transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0", "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0", className, ), @@ -699,7 +699,7 @@ function SidebarGroupLabel({ className, render, ...props }: useRender.ComponentP function SidebarGroupAction({ className, render, ...props }: useRender.ComponentProps<"button">) { const defaultProps = { className: cn( - "absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-lg p-0 text-sidebar-foreground outline-hidden ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg:not([class*='size-'])]:size-4 [&>svg]:shrink-0", + "absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-lg p-0 text-sidebar-foreground outline-hidden ring-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg:not([class*='size-'])]:size-4 [&>svg]:shrink-0", // Increases the hit area of the button on mobile. "after:-inset-2 after:absolute md:after:hidden", "group-data-[collapsible=icon]:hidden", @@ -750,7 +750,7 @@ function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) { } const sidebarMenuButtonVariants = cva( - "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-lg p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pe-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg:not([class*='size-'])]:size-4 [&>svg]:shrink-0", + "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-lg p-2 text-left text-sm outline-hidden ring-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pe-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg:not([class*='size-'])]:size-4 [&>svg]:shrink-0", { defaultVariants: { size: "default", @@ -834,7 +834,7 @@ function SidebarMenuAction({ }) { const defaultProps = { className: cn( - "absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-lg p-0 text-sidebar-foreground outline-hidden ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 peer-hover/menu-button:text-sidebar-accent-foreground [&>svg:not([class*='size-'])]:size-4 [&>svg]:shrink-0", + "absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-lg p-0 text-sidebar-foreground outline-hidden ring-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 peer-hover/menu-button:text-sidebar-accent-foreground [&>svg:not([class*='size-'])]:size-4 [&>svg]:shrink-0", // Increases the hit area of the button on mobile. "after:-inset-2 after:absolute md:after:hidden", "peer-data-[size=sm]/menu-button:top-1", @@ -946,7 +946,7 @@ function SidebarMenuSubButton({ }) { const defaultProps = { className: cn( - "-translate-x-px flex h-7 min-w-0 items-center gap-2 overflow-hidden rounded-lg px-2 text-sidebar-foreground outline-hidden ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg:not([class*='size-'])]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground", + "-translate-x-px flex h-7 min-w-0 items-center gap-2 overflow-hidden rounded-lg px-2 text-sidebar-foreground outline-hidden ring-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg:not([class*='size-'])]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground", "data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground", size === "sm" && "text-xs", size === "md" && "text-sm",