Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
ae5ea3b
Add completion signal protocol and enhance loop status reporting
chriswritescode-dev Mar 28, 2026
fee2ea6
Add TUI sidebar plugin with loop status display
chriswritescode-dev Mar 29, 2026
4ca0ec5
Configure TypeScript and package exports for TUI
chriswritescode-dev Mar 29, 2026
4fd500a
Add TUI toast notifications for loop completion
chriswritescode-dev Mar 29, 2026
b4d041a
Document TUI configuration options
chriswritescode-dev Mar 29, 2026
9fb8c39
Export compareVersions utility for TUI usage
chriswritescode-dev Mar 29, 2026
fba7ba1
Remove legacy inject-version script
chriswritescode-dev Mar 29, 2026
fd22f0c
Simplify completion promise and add reload action
chriswritescode-dev Mar 29, 2026
006ad50
Update pnpm lock file
chriswritescode-dev Mar 29, 2026
d2b7a1d
Pass workspaceID to loop session creation for TUI detection
chriswritescode-dev Mar 29, 2026
bdd8928
Add workspace support and TUI loop state persistence
chriswritescode-dev Mar 29, 2026
082b52b
Remove workspace API integration from loop system
chriswritescode-dev Mar 30, 2026
b1765eb
Migrate from zodResolver to standardSchemaResolver and improve TUI po…
chriswritescode-dev Mar 30, 2026
5aec209
Harden loop system and add per-agent config overrides
chriswritescode-dev Mar 30, 2026
f35aec6
Pass worktreeDir to selectSession for worktree loop navigation
chriswritescode-dev Mar 30, 2026
09ff795
Refactor memory plugin: Split monolithic index.ts into use-case modules
chriswritescode-dev Mar 30, 2026
808cb9a
Add branch-aware review findings to loop service
chriswritescode-dev Mar 30, 2026
28f2027
Export memory plugin as module object
chriswritescode-dev Mar 30, 2026
079ccf5
fix inverted worktree/in-place mode label
chriswritescode-dev Mar 30, 2026
9ab9861
Fix proxy response handling and improve error handling
chriswritescode-dev Mar 30, 2026
2804431
Fix Docker build by omitting dev dependencies in memory plugin install
chriswritescode-dev Mar 30, 2026
920f2b6
Update memory agent documentation and tool references
chriswritescode-dev Mar 30, 2026
0d3b0ea
Make memory plugin install opt-in via environment variable
chriswritescode-dev Mar 30, 2026
76c1e39
Fix Docker container crash by copying backend node_modules from deps …
chriswritescode-dev Mar 30, 2026
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
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,8 @@ PASSKEY_ORIGIN=http://localhost:5003
# VITE_OPENCODE_PORT=5551
# VITE_MAX_FILE_SIZE_MB=50
# VITE_MAX_UPLOAD_SIZE_MB=50

# ============================================
# Plugins
# ============================================
INSTALL_MEMORY_PLUGIN=true
22 changes: 4 additions & 18 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ COPY --chown=node:node package.json pnpm-workspace.yaml pnpm-lock.yaml ./
COPY --chown=node:node shared/package.json ./shared/
COPY --chown=node:node backend/package.json ./backend/
COPY --chown=node:node frontend/package.json ./frontend/
COPY --chown=node:node packages/memory ./packages/memory/

RUN pnpm install --frozen-lockfile

Expand All @@ -53,10 +52,8 @@ COPY backend ./backend
COPY frontend/src ./frontend/src
COPY frontend/public ./frontend/public
COPY frontend/index.html frontend/vite.config.ts frontend/tsconfig*.json frontend/components.json frontend/eslint.config.js ./frontend/
COPY packages/memory ./packages/memory

RUN pnpm --filter frontend build
RUN pnpm --filter @opencode-manager/memory build

FROM base AS runner

Expand Down Expand Up @@ -84,31 +81,20 @@ ENV OPENCODE_SERVER_PORT=5551
ENV DATABASE_PATH=/app/data/opencode.db
ENV WORKSPACE_PATH=/workspace
ENV NODE_PATH=/opt/opencode-plugins/node_modules
ENV INSTALL_MEMORY_PLUGIN=true

COPY --from=deps --chown=node:node /app/node_modules ./node_modules
COPY --from=builder /app/shared ./shared
COPY --from=builder /app/backend ./backend
COPY --from=builder /app/frontend/dist ./frontend/dist
COPY package.json pnpm-workspace.yaml ./

RUN mkdir -p /app/backend/node_modules/@opencode-manager && \
ln -s /app/shared /app/backend/node_modules/@opencode-manager/shared

COPY --from=builder /app/packages/memory /opt/opencode-plugins/src

RUN cd /opt/opencode-plugins/src && npm install

RUN mkdir -p /opt/opencode-plugins/node_modules/@opencode-manager/memory && \
cp -r /opt/opencode-plugins/src/dist/* /opt/opencode-plugins/node_modules/@opencode-manager/memory/ && \
cp /opt/opencode-plugins/src/package.json /opt/opencode-plugins/node_modules/@opencode-manager/memory/ && \
cp /opt/opencode-plugins/src/config.jsonc /opt/opencode-plugins/node_modules/@opencode-manager/memory/config.jsonc 2>/dev/null || true && \
cp -r /opt/opencode-plugins/src/node_modules/* /opt/opencode-plugins/node_modules/ 2>/dev/null || true
COPY --from=deps --chown=node:node /app/backend/node_modules ./backend/node_modules

COPY scripts/docker-entrypoint.sh /docker-entrypoint.sh
RUN chmod +x /docker-entrypoint.sh

RUN mkdir -p /workspace /app/data && \
chown -R node:node /workspace /app/data
RUN mkdir -p /workspace /app/data /opt/opencode-plugins && \
chown -R node:node /workspace /app/data /opt/opencode-plugins

EXPOSE 5003 5100 5101 5102 5103

Expand Down
14 changes: 10 additions & 4 deletions backend/src/services/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,13 +118,16 @@ export async function proxyRequest(request: Request) {
})

const responseHeaders: Record<string, string> = {}
const skipHeaders = new Set(['connection', 'transfer-encoding', 'content-encoding', 'content-length'])
response.headers.forEach((value, key) => {
if (!['connection', 'transfer-encoding'].includes(key.toLowerCase())) {
if (!skipHeaders.has(key.toLowerCase())) {
responseHeaders[key] = value
}
})

return new Response(response.body, {
const body = await response.text()

return new Response(body, {
status: response.status,
statusText: response.statusText,
headers: responseHeaders,
Expand Down Expand Up @@ -159,13 +162,16 @@ export async function proxyToOpenCodeWithDirectory(
})

const responseHeaders: Record<string, string> = {}
const skipHeaders = new Set(['connection', 'transfer-encoding', 'content-encoding', 'content-length'])
response.headers.forEach((value, key) => {
if (!['connection', 'transfer-encoding'].includes(key.toLowerCase())) {
if (!skipHeaders.has(key.toLowerCase())) {
responseHeaders[key] = value
}
})

return new Response(response.body, {
const responseBody = await response.text()

return new Response(responseBody, {
status: response.status,
statusText: response.statusText,
headers: responseHeaders,
Expand Down
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ services:
- VAPID_PUBLIC_KEY=${VAPID_PUBLIC_KEY:-}
- VAPID_PRIVATE_KEY=${VAPID_PRIVATE_KEY:-}
- VAPID_SUBJECT=${VAPID_SUBJECT:-}
- INSTALL_MEMORY_PLUGIN=${INSTALL_MEMORY_PLUGIN:-true}
volumes:
- opencode-workspace:/workspace
- opencode-data:/app/data
Expand Down
8 changes: 8 additions & 0 deletions docs/features/memory.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@ The file is only created if it does not already exist. The config is validated o
"model": "",
"minAudits": 1,
"stallTimeoutMs": 60000
},
"tui": {
"sidebar": true,
"showLoops": true,
"showVersion": true
}
}
```
Expand Down Expand Up @@ -140,6 +145,9 @@ Set `baseUrl` to point at any OpenAI-compatible self-hosted service (vLLM, Ollam
| `loop.model` | Model override for loop sessions (`provider/model`), falls back to `executionModel` | — |
| `loop.minAudits` | Minimum audit iterations required before completion | `1` |
| `loop.stallTimeoutMs` | Watchdog stall detection timeout (ms) | `60000` |
| `tui.sidebar` | Show the Memory plugin sidebar in the TUI | `true` |
| `tui.showLoops` | Show active loops in the sidebar | `true` |
| `tui.showVersion` | Show version number in sidebar title | `true` |

!!! note "Deprecated Options"
The `ralph.*` prefix is deprecated but still accepted for backward compatibility. Use `loop.*` instead.
Expand Down
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
},
"devDependencies": {
"@eslint/js": "^9.36.0",
"@standard-schema/spec": "^1.1.0",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.1",
"@testing-library/user-event": "^14.6.1",
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/memory/KvFormDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { standardSchemaResolver } from '@hookform/resolvers/standard-schema'
import { z } from 'zod'
import { useCreateKvEntry, useUpdateKvEntry } from '@/hooks/useMemories'
import type { KvEntry, CreateKvEntryRequest, UpdateKvEntryRequest } from '@opencode-manager/shared/types'
Expand Down Expand Up @@ -44,7 +44,7 @@ export function KvFormDialog({ entry, projectId, open, onOpenChange }: KvFormDia
reset,
formState: { errors },
} = useForm<KvFormData>({
resolver: zodResolver(kvSchema),
resolver: standardSchemaResolver(kvSchema),
defaultValues: {
key: '',
data: '',
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/memory/MemoryFormDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useEffect } from 'react'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { standardSchemaResolver } from '@hookform/resolvers/standard-schema'
import { z } from 'zod'
import { useCreateMemory, useUpdateMemory } from '@/hooks/useMemories'
import type { Memory, CreateMemoryRequest, UpdateMemoryRequest } from '@opencode-manager/shared/types'
Expand Down Expand Up @@ -43,7 +43,7 @@ export function MemoryFormDialog({ memory, projectId, open, onOpenChange }: Memo
reset,
formState: { errors },
} = useForm<MemoryFormData>({
resolver: zodResolver(memorySchema),
resolver: standardSchemaResolver(memorySchema),
defaultValues: {
content: '',
scope: 'context',
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/settings/AgentDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { standardSchemaResolver } from '@hookform/resolvers/standard-schema'
import { z } from 'zod'
import { useMemo, useEffect } from 'react'
import { useQuery } from '@tanstack/react-query'
Expand Down Expand Up @@ -110,7 +110,7 @@ export function AgentDialog({ open, onOpenChange, onSubmit, editingAgent }: Agen
}

const form = useForm<AgentFormValues>({
resolver: zodResolver(agentFormSchema),
resolver: standardSchemaResolver(agentFormSchema),
defaultValues: getDefaultValues(editingAgent)
})

Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/settings/CommandDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { standardSchemaResolver } from "@hookform/resolvers/standard-schema";
import { z } from "zod";
import { Button } from "@/components/ui/button";
import {
Expand Down Expand Up @@ -63,7 +63,7 @@ export function CommandDialog({
editingCommand,
}: CommandDialogProps) {
const form = useForm<CommandFormValues>({
resolver: zodResolver(commandFormSchema),
resolver: standardSchemaResolver(commandFormSchema),
defaultValues: {
name: editingCommand?.name || "",
template: editingCommand?.command.template || "",
Expand Down
36 changes: 35 additions & 1 deletion frontend/src/components/settings/MemoryPluginConfig.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ export function MemoryPluginConfig({ memoryPluginEnabled, onToggle }: MemoryPlug
})
}

const handleNestedChange = <K extends 'logging' | 'compaction' | 'memoryInjection' | 'messagesTransform'>(
const handleNestedChange = <K extends 'logging' | 'compaction' | 'memoryInjection' | 'messagesTransform' | 'tui'>(
section: K,
field: string,
value: string | number | boolean | undefined,
Expand Down Expand Up @@ -560,6 +560,40 @@ export function MemoryPluginConfig({ memoryPluginEnabled, onToggle }: MemoryPlug
/>
</div>
</div>

<div className="space-y-4">
<div className="flex items-center gap-2 mb-3">
<Layers className="h-4 w-4 text-indigo-500" />
<span className="text-sm font-medium">TUI</span>
</div>

<div className="flex items-center justify-between">
<Label htmlFor="tuiSidebar">Show Sidebar</Label>
<Switch
id="tuiSidebar"
checked={displayConfig.tui?.sidebar ?? true}
onCheckedChange={(checked) => handleNestedChange('tui', 'sidebar', checked)}
/>
</div>

<div className="flex items-center justify-between">
<Label htmlFor="tuiShowLoops">Show Loops</Label>
<Switch
id="tuiShowLoops"
checked={displayConfig.tui?.showLoops ?? true}
onCheckedChange={(checked) => handleNestedChange('tui', 'showLoops', checked)}
/>
</div>

<div className="flex items-center justify-between">
<Label htmlFor="tuiShowVersion">Show Version</Label>
<Switch
id="tuiShowVersion"
checked={displayConfig.tui?.showVersion ?? true}
onCheckedChange={(checked) => handleNestedChange('tui', 'showVersion', checked)}
/>
</div>
</div>
</div>

{displayConfig.dataDir && (
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/settings/STTSettings.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { standardSchemaResolver } from '@hookform/resolvers/standard-schema'
import { z } from 'zod'
import { useSettings } from '@/hooks/useSettings'
import { useSTT } from '@/hooks/useSTT'
Expand Down Expand Up @@ -50,7 +50,7 @@ export function STTSettings() {
const isWebSpeechAvailable = isWebRecognitionSupported()

const form = useForm<STTFormValues>({
resolver: zodResolver(sttFormSchema),
resolver: standardSchemaResolver(sttFormSchema),
defaultValues: {
...DEFAULT_STT_CONFIG,
model: DEFAULT_STT_CONFIG.model || 'whisper-1',
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/settings/SkillDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { standardSchemaResolver } from '@hookform/resolvers/standard-schema'
import { z } from 'zod'
import { useEffect, useState } from 'react'
import { Button } from '@/components/ui/button'
Expand Down Expand Up @@ -58,7 +58,7 @@ export function SkillDialog({ open, onOpenChange, onSubmit, editingSkill }: Skil
}

const form = useForm<SkillFormValues>({
resolver: zodResolver(skillFormSchema),
resolver: standardSchemaResolver(skillFormSchema),
defaultValues: getDefaultValues(editingSkill)
})

Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/settings/TTSSettings.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useEffect, useState, useRef } from 'react'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { standardSchemaResolver } from '@hookform/resolvers/standard-schema'
import { z } from 'zod'
import { useSettings } from '@/hooks/useSettings'
import { useTTS } from '@/hooks/useTTS'
Expand Down Expand Up @@ -85,7 +85,7 @@ export function TTSSettings() {
const lastSavedDataRef = useRef<TTSFormValues | null>(null)

const form = useForm<TTSFormValues>({
resolver: zodResolver(ttsFormSchema),
resolver: standardSchemaResolver(ttsFormSchema),
defaultValues: DEFAULT_TTS_CONFIG,
})

Expand Down
8 changes: 3 additions & 5 deletions frontend/src/hooks/useOpenCode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { useMemo, useRef, useEffect, useCallback, useState } from "react";
import { OpenCodeClient } from "../api/opencode";
import { API_BASE_URL } from "../config";
import { fetchWrapper } from "../api/fetchWrapper";
import { fetchWrapper, FetchError } from "../api/fetchWrapper";
import { cancelLoop } from "../api/memory";
import type {
Message,
Expand Down Expand Up @@ -355,10 +355,8 @@ export const useSendPrompt = (opcodeUrl: string | null | undefined, directory?:
const { sessionID } = variables;
const messagesQueryKey = ["opencode", "messages", opcodeUrl, sessionID, directory];

const axiosError = error as { code?: string; response?: unknown };
const isNetworkError = axiosError.code === 'ECONNABORTED' ||
axiosError.code === 'ERR_NETWORK' ||
!axiosError.response;
const isNetworkError = error instanceof TypeError ||
(error instanceof FetchError && error.code === 'TIMEOUT');

if (isNetworkError) {
return;
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/hooks/useSSE.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,8 +308,8 @@ export const useSSE = (opcodeUrl: string | null | undefined, directory?: string,
})
}
} catch (err) {
if (err instanceof Error && !err.message.includes('aborted')) {
throw err
if (err instanceof Error && !err.message.includes('aborted') && import.meta.env.DEV) {
console.warn('Failed to fetch initial session data:', err)
}
}
}, [client, setSessionStatus])
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/pages/Login.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useState } from 'react'
import { useLoaderData } from 'react-router-dom'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { standardSchemaResolver } from '@hookform/resolvers/standard-schema'
import { z } from 'zod'
import { useAuth } from '@/hooks/useAuth'
import { useTheme } from '@/hooks/useTheme'
Expand Down Expand Up @@ -33,7 +33,7 @@ export function Login() {
handleSubmit,
formState: { errors },
} = useForm<LoginFormData>({
resolver: zodResolver(loginSchema),
resolver: standardSchemaResolver(loginSchema),
})

const onSubmit = async (data: LoginFormData) => {
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/pages/Register.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useState } from 'react'
import { Link, useLoaderData } from 'react-router-dom'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { standardSchemaResolver } from '@hookform/resolvers/standard-schema'
import { z } from 'zod'
import { useAuth } from '@/hooks/useAuth'
import { useTheme } from '@/hooks/useTheme'
Expand Down Expand Up @@ -36,7 +36,7 @@ export function Register() {
handleSubmit,
formState: { errors },
} = useForm<RegisterFormData>({
resolver: zodResolver(registerSchema),
resolver: standardSchemaResolver(registerSchema),
})

const onSubmit = async (data: RegisterFormData) => {
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/pages/Setup.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useState } from 'react'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { standardSchemaResolver } from '@hookform/resolvers/standard-schema'
import { z } from 'zod'
import { useAuth } from '@/hooks/useAuth'
import { useTheme } from '@/hooks/useTheme'
Expand Down Expand Up @@ -29,7 +29,7 @@ export function Setup() {
handleSubmit,
formState: { errors },
} = useForm<SetupFormData>({
resolver: zodResolver(setupSchema),
resolver: standardSchemaResolver(setupSchema),
})

const onSubmit = async (data: SetupFormData) => {
Expand Down
Loading
Loading