Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
9a63213
feat: warm the agent model up while the user types the first task
claude Oct 24, 2026
51423e2
fix(cli): name the real cause of errors, show RAG sources, use the in…
claude Oct 24, 2026
d844e94
fix(pwa): only ask to prepare the network when LAN access is enabled
claude Oct 24, 2026
eeac061
test: stop the suite from shutting down the developer's own services
claude Oct 24, 2026
eb2fe5b
fix: show a stopped service as stopped, not "stopped not found"
claude Oct 24, 2026
b019514
feat(cli): route file and git requests to the agent
claude Oct 24, 2026
0843019
fix(cli): readable model sizes and a translated sandbox row in doctor
claude Oct 24, 2026
46e5bad
fix(install): the dry-run no longer announces a LAN link when LAN acc…
claude Oct 24, 2026
78a87ea
fix(cli): only a 404 that names the model is reported as a missing model
claude Oct 24, 2026
3ac68ce
fix(rag): recognise a refusal only at the start of an answer
claude Oct 24, 2026
dea82d7
feat(cli): trinaxai network off, and say whether LAN access is on
claude Oct 24, 2026
e27f4ec
docs: troubleshooting rows quote the exact CLI errors and the way bac…
claude Oct 24, 2026
44f98ad
fix(pwa): flag failed agent tool steps with a warning, not a green check
claude Oct 24, 2026
93ad7f9
feat(agent): explain a failed git commit (nothing staged / clean tree…
claude Oct 24, 2026
cf25a45
feat(agent): refuse a plain git commit with nothing staged before ask…
claude Oct 24, 2026
b45963e
fix(cli): doctor does not repeat the start hint when the error alread…
claude Oct 24, 2026
1991754
fix(cli): decide the doctor hint by error category, not by its text
claude Oct 24, 2026
30168d5
fix(cli): URLs are not workspace files and conceptual git/read questi…
claude Oct 24, 2026
679ea8f
fix: apply review findings (env parsing, abstention, legacy model, TL…
claude Oct 24, 2026
e4ce8d4
feat(cli): route 'show me the git diff' style requests to the agent
claude Oct 24, 2026
ab4a0a1
fix(cli): follow a local API served without HTTPS; network status sto…
claude Oct 24, 2026
081b947
feat(index): name the files skipped for being too large
claude Oct 24, 2026
d7c8caa
fix(cli): name a missing collection and reach a plain-HTTP API when s…
claude Oct 24, 2026
74b14e7
fix(index): say why a file with no text is left out
claude Oct 24, 2026
6ba8114
fix(cli): apply second review (question-form veto, path vs URL, stric…
claude Oct 24, 2026
ce8c6ef
fix(install): no color codes when output is piped or NO_COLOR is set
claude Oct 24, 2026
d4508f9
fix(scripts): update, uninstall and setup print plain text when piped…
claude Oct 24, 2026
3c01f4e
fix(agent): python3, python, cc and awk resolve inside the command sa…
claude Oct 24, 2026
b666025
feat(cli): trinaxai research takes the topic as plain words
claude Oct 24, 2026
1505e6d
fix(cli): a wait-limit timeout is not a network failure; TRINAXAI_CLI…
claude Oct 24, 2026
fcc9eda
feat(cli): trinaxai research --web grounds the research on web search
claude Oct 24, 2026
7c77da1
fix(cli): research lets the server choose reasoning unless --thinking…
claude Oct 24, 2026
0914d87
fix(agent): ask once for the run the user requested; 'mutated' now tr…
claude Oct 24, 2026
74faea7
fix(agent): the one-file shortcut no longer cuts short a request for …
claude Oct 24, 2026
d9d7c93
feat(agent): chase a failed run before reporting success
claude Oct 24, 2026
7f728c0
fix(agent): drop earlier failed run reports once a later run passes
claude Oct 24, 2026
ed1ab63
feat(agent): search for the old name after a rename and send the mode…
claude Oct 24, 2026
0e2882e
fix(security): no credentials over the unverified plain-HTTP fallback…
claude Oct 24, 2026
282fd95
fix: apply third review (no-color typo, stricter agent follow-through…
claude Oct 24, 2026
2a87a6c
docs: README command table lists research --web and network off
claude Oct 24, 2026
35452d4
fix(agent): a scripted run says why an edit or command was refused
claude Oct 24, 2026
4834586
refactor: one helper for the agent's corrective nudges and for walkin…
claude Oct 24, 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
3 changes: 2 additions & 1 deletion README.es.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,10 @@ El instalador detecta CPU, memoria, GPU y VRAM. Consulta
| Abrir el asistente interactivo | `trinaxai chat` |
| Hacer una pregunta | `trinaxai ask "..." --engine rag` |
| Indexar una carpeta | `trinaxai index ./documentos` |
| Ejecutar investigación | `trinaxai research --query "..." --depth 2` |
| Ejecutar investigación (añade `--web` para apoyarla en búsqueda web) | `trinaxai research "..." --depth 2` |
| Usar el agente | `trinaxai agent --workspace .` |
| Revisar servicios | `trinaxai status` |
| Ver o cambiar quién puede abrir TrinaxAI | `trinaxai network` / `trinaxai network off` |
| Iniciar o detener servicios | `trinaxai start` / `trinaxai stop` |
| Diagnosticar | `trinaxai doctor --strict` |
| Actualizar | `trinaxai update` |
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,10 @@ Use the CLI from any directory after setup:
| Open the interactive assistant | `trinaxai chat` |
| Ask one question | `trinaxai ask "..." --engine rag` |
| Index a folder | `trinaxai index ./documents` |
| Run deep research | `trinaxai research --query "..." --depth 2` |
| Run deep research (add `--web` to ground it on web search) | `trinaxai research "..." --depth 2` |
| Use the coding agent | `trinaxai agent --workspace .` |
| Check services | `trinaxai status` |
| See or change who can reach TrinaxAI | `trinaxai network` / `trinaxai network off` |
| Start or stop services | `trinaxai start` / `trinaxai stop` |
| Diagnose a problem | `trinaxai doctor --strict` |
| Update the installation | `trinaxai update` |
Expand Down
8 changes: 7 additions & 1 deletion app/services/rag_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@
# lowest near-zero candidates as evidence.
RAG_MIN_SCORE = config._env_float("TRINAXAI_RAG_MIN_SCORE", 0.015, minimum=0.0, maximum=1.0)
_ABSTENTION_MESSAGES = frozenset({NO_INDEX_MSG, NO_INDEX_MSG_ES, EMPTY_COLLECTION_MSG, NO_RELEVANT_RESULTS_MSG})
_ABSTENTION_WINDOW = 300 # characters at the start of a long answer where a refusal can appear
_SHORT_ANSWER = 500 # a refusal can come anywhere in an answer this short
_ABSTENTION_MARKERS = (
"no se encontró",
"no encontre",
Expand Down Expand Up @@ -271,7 +273,11 @@ def _is_rag_abstention(content: str, *, rag_requested: bool) -> bool:
return True
text = str(content or "").replace("\u2019", "'").casefold()
normalized = re.sub(r"\s+", " ", text).strip()
return any(marker in normalized for marker in _ABSTENTION_MARKERS)
# A refusal opens the answer or is all of it. The same words deep inside a long
# explanation ("... if it fails or is not available, fall back to ...") belong to a
# grounded answer, whose sources must not be dropped.
scanned = normalized if len(normalized) <= _SHORT_ANSWER else normalized[:_ABSTENTION_WINDOW]
return any(marker in scanned for marker in _ABSTENTION_MARKERS)


def _thinking_preference(value: bool | None) -> bool:
Expand Down
3 changes: 2 additions & 1 deletion app/services/rag_streaming.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ def _sse_error(exc: Exception) -> str:
type(exc).__name__,
)
error = info.to_client_dict()
error["recovery"] = f"{error['recovery']} Please retry."
if info.retryable: # retrying will not install a missing model or fix a bad request
error["recovery"] = f"{error['recovery']} Please retry."
return _sse({"trinaxai_error": error, "trinaxai_finish": _completion_metadata("error")})


Expand Down
3 changes: 2 additions & 1 deletion app/services/research_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -755,7 +755,8 @@ def _research_sse(payload: dict) -> str:
def _research_sse_error(exc: Exception) -> str:
info = classify_error(exc, status_code=503)
error = info.to_client_dict()
error["recovery"] = f"{error['recovery']} Please retry."
if info.retryable: # retrying will not install a missing model or fix a bad request
error["recovery"] = f"{error['recovery']} Please retry."
return _research_sse(
{
"trinaxai_error": error,
Expand Down
44 changes: 43 additions & 1 deletion chat-pwa/server.integration.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,44 @@ test('network status reads CORS origins from disk instead of the stale service e
status.configurationCurrent,
status.urls.every((url) => (onDisk || 'https://stale.invalid:3334').includes(url)),
);
// This computer only: an out-of-date LAN configuration affects nobody, so it never asks for a refresh.
assert.equal(status.lanAccess, false);
assert.equal(status.needsRefresh, false);
} finally {
gateway.kill();
}
});

test('a gateway open to the local network asks for a refresh when its LAN configuration is stale', async () => {
ensureFrontendFixture();
const gatewayPort = await unusedPort();
const gateway = spawn(process.execPath, ['server.mjs'], {
cwd: new URL('.', import.meta.url),
env: {
...process.env,
CI: 'true',
TRINAXAI_PWA_HOST: '0.0.0.0',
TRINAXAI_PWA_PORT: String(gatewayPort),
TRINAXAI_ALLOW_INSECURE_HTTP: '1',
TRINAXAI_CORS_ORIGINS: 'https://stale.invalid:3334',
},
stdio: ['ignore', 'pipe', 'pipe'],
});
try {
await new Promise((resolve, reject) => {
const timeout = setTimeout(() => reject(new Error('gateway startup timed out')), 5000);
gateway.once('exit', (code) => reject(new Error(`gateway exited with ${code}`)));
gateway.stdout.on('data', (chunk) => {
if (!String(chunk).includes('listening')) return;
clearTimeout(timeout);
resolve();
});
});

const status = await (await fetch(`http://127.0.0.1:${gatewayPort}/api/network`)).json();
assert.equal(status.lanAccess, true);
if (status.configurationCurrent === false) assert.equal(status.needsRefresh, true);
assert.equal(status.needsRefresh, status.configurationCurrent === false || status.certificateCurrent === false);
} finally {
gateway.kill();
}
Expand Down Expand Up @@ -224,7 +262,11 @@ test('production gateway preserves credentials, replaces proxy identity, and rej
assert.equal(networkBody.capabilities.manageSystem, true);
assert.equal(typeof networkBody.existingInstallation, 'boolean');
assert.equal(networkBody.refreshCommand, 'trinaxai network refresh');
assert.match(networkBody.recommendedUrl, /^https:\/\/(?:\d{1,3}\.){3}\d{1,3}:/);
// Without a private LAN interface (a container, an offline VM) the stable `.local` name is recommended.
assert.match(
networkBody.recommendedUrl,
privateLanAddress() ? /^https:\/\/(?:\d{1,3}\.){3}\d{1,3}:/ : /^https:\/\/[^/]+\.local:\d+$/,
);

const forbiddenRefresh = await fetch(`http://127.0.0.1:${gatewayPort}/api/system/network-refresh`, {
method: 'POST',
Expand Down
7 changes: 6 additions & 1 deletion chat-pwa/server.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -509,16 +509,21 @@ function networkStatus() {
certificateCurrent = subjectAltName.includes(`DNS:${stableHost}`)
&& addresses.every((address) => subjectAltName.includes(`IP Address:${address}`));
} catch { /* PFX-only Windows installs are checked by the refresh command. */ }
// With the gateway bound to this computer only, a changed LAN address or an
// out-of-date LAN certificate affects nobody, so it must not nag the user (a
// laptop that moves between Wi-Fi networks would otherwise see it every time).
const lanReachable = !isLoopbackAddress(host);
return {
online: true,
existingInstallation: hasExistingInstallation(),
hostname,
addresses,
urls,
recommendedUrl: urls[0],
lanAccess: lanReachable,
configurationCurrent,
certificateCurrent,
needsRefresh: !configurationCurrent || certificateCurrent === false,
needsRefresh: lanReachable && (!configurationCurrent || certificateCurrent === false),
refreshCommand: 'trinaxai network refresh',
};
}
Expand Down
15 changes: 15 additions & 0 deletions chat-pwa/src/components/agent/AgentInterfaceView.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,21 @@ describe('AgentInterfaceView', () => {
expect(screen.getAllByText(/git push origin main/)).toHaveLength(2);
});

it('marks tools that could not do their job with a warning instead of a green check', () => {
const steps = [
{ id: 'f1', tool: 'git', dangerous: true, args: { args: 'commit -m x' }, status: 'done' as const, result: '[tool_status=degraded; tool=git]\nWhat happened: git could not complete this request.' },
{ id: 'f2', tool: 'run_command', dangerous: true, args: { command: 'pytest' }, status: 'done' as const, result: '[exit 1; sandboxed, network=off]\nFAILED' },
{ id: 'ok', tool: 'run_command', dangerous: true, args: { command: 'ls' }, status: 'done' as const, result: '[exit 0; sandboxed, network=off]\nfile.txt' },
{ id: 'plain', tool: 'read_file', dangerous: false, args: { path: 'a.py' }, status: 'done' as const, result: 'print(1)' },
{ id: 'denied', tool: 'write_file', dangerous: true, args: { path: 'b.py' }, status: 'denied' as const, result: '[exit 2] user said no' },
];
render(<AgentInterfaceView {...props({ turns: [{ role: 'assistant' as const, content: '', steps }] })} />);

// Only the two steps that failed are flagged; a clean exit, a plain result and a denial are not.
expect(screen.getAllByRole('img', { name: 'agentStepFailed' })).toHaveLength(2);
expect(document.querySelectorAll('svg.text-green-500')).toHaveLength(2);
});

it('covers editing, running activity, attached files, and stop states', () => {
const p = props({
isDark: true, running: true, editingIndex: 0, editingText: 'edit me',
Expand Down
25 changes: 18 additions & 7 deletions chat-pwa/src/components/agent/AgentTurns.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Dispatch, RefObject, SetStateAction } from 'react';
import { AnimatePresence, motion, useReducedMotion } from 'framer-motion';
import { MdCheck, MdClose, MdContentCopy, MdEdit, MdRefresh, MdStop, MdUploadFile, MdVolumeUp } from 'react-icons/md';
import { MdCheck, MdClose, MdContentCopy, MdEdit, MdRefresh, MdStop, MdUploadFile, MdVolumeUp, MdWarningAmber } from 'react-icons/md';
import type { Translate } from '../chat/types';
import ChatMarkdown from '../chat/ChatMarkdown';
import type { AgentStep, AgentTurn } from './agentTypes';
Expand Down Expand Up @@ -261,13 +261,24 @@ interface StepCardProps {
t: Translate;
}

// A tool that ran but could not do its job (degraded result, or a command that exited non-zero)
// is still "done" for the protocol; a green check would tell the user the opposite of what happened.
const FAILED_RESULT = /^\[(?:tool_status=degraded\b|exit (?!0\b)-?\d+)/;

function AgentStepCard({ step, isDark, cardBg, subtle, onApprove, t }: StepCardProps) {
const statusIcon = {
running: <span className="h-2 w-2 animate-pulse rounded-full bg-[#006bbd]" />,
awaiting: <span className="h-2 w-2 rounded-full bg-amber-400" />,
done: <MdCheck size={14} className="text-green-500" />,
denied: <MdClose size={14} className="text-red-500" />,
}[step.status];
const failed = step.status === 'done' && FAILED_RESULT.test(step.result ?? '');
const statusIcon = failed
? (
<span role="img" aria-label={t('agentStepFailed')} title={t('agentStepFailed')} className="inline-flex">
<MdWarningAmber size={14} className="text-amber-500" />
</span>
)
: {
running: <span className="h-2 w-2 animate-pulse rounded-full bg-[#006bbd]" />,
awaiting: <span className="h-2 w-2 rounded-full bg-amber-400" />,
done: <MdCheck size={14} className="text-green-500" />,
denied: <MdClose size={14} className="text-red-500" />,
}[step.status];

const summary = argSummary(step.tool, step.args);
const dangerKey = step.tool === 'git' && GIT_REMOTE_COMMAND.test(step.args.args ?? step.args.command ?? '')
Expand Down
2 changes: 2 additions & 0 deletions chat-pwa/src/i18n/translations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ export const translations = {
agentReject: 'Rechazar',
agentApprovePrompt: '¿Permitir esta acción?',
agentFailed: 'El agente falló.',
agentStepFailed: 'La herramienta no pudo completar la acción.',
toolWrite: 'El agente quiere crear o sobrescribir un archivo.',
toolEdit: 'El agente quiere modificar un archivo.',
toolRun: 'El agente quiere ejecutar un comando de shell.',
Expand Down Expand Up @@ -1337,6 +1338,7 @@ export const translations = {
agentReject: 'Reject',
agentApprovePrompt: 'Allow this action?',
agentFailed: 'The agent failed.',
agentStepFailed: 'The tool could not complete the action.',
toolWrite: 'The agent wants to create or overwrite a file.',
toolEdit: 'The agent wants to modify a file.',
toolRun: 'The agent wants to run a shell command.',
Expand Down
Loading
Loading