Skip to content

Commit b7bc41f

Browse files
pnpm and other fixes
1 parent 0e86e9d commit b7bc41f

15 files changed

Lines changed: 149 additions & 47 deletions

File tree

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ WORKDIR $homedir
2323
RUN mkdir ".husky"
2424
COPY .husky/install.mjs .husky/install.mjs
2525

26-
COPY package*.json ./
26+
COPY package*.json pnpm-lock.yaml ./
2727
RUN pnpm install
2828

2929
COPY . .

build.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,8 @@ async function runNpmScriptOrCommandInDirs(scriptOrCommandName, directories) {
111111
currentNpmSubCommandForDir += ':ci';
112112
}
113113

114-
const commandToExecuteInDir = `npm ${currentNpmSubCommandForDir}`;
114+
const pkgMgr = dirInfo.path.includes('frontend') ? 'npm' : 'pnpm';
115+
const commandToExecuteInDir = `${pkgMgr} ${currentNpmSubCommandForDir}`;
115116

116117
// Use the base script/command name for the task identifier for consistency
117118
const taskName = `${dirInfo.name}-${scriptOrCommandName}`; // e.g., "frontend-build", "root-install"
@@ -144,7 +145,7 @@ async function runNpmScriptOrCommandInDirs(scriptOrCommandName, directories) {
144145
}
145146
});
146147

147-
const overallCommandDescription = `npm ${baseNpmSubCommand}`;
148+
const overallCommandDescription = `${pkgMgr} ${baseNpmSubCommand}`;
148149
if (anyFailed) {
149150
console.error(`\n>>> "${overallCommandDescription}" failed in one or more directories. <<<`);
150151
return false; // Indicate failure

frontend/src/app/modules/prompts/form/prompt-form.component.html

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -290,20 +290,29 @@ <h4 class="text-xl font-semibold pb-4">
290290
<div class="flex items-center justify-between mb-4">
291291
<h2 class="text-xl font-semibold">Response</h2>
292292
<div class="flex items-center gap-2">
293-
<button *ngIf="generationResponse()"
293+
<button *ngIf="generationResult()"
294294
mat-stroked-button
295295
color="primary"
296296
(click)="addResponseToPrompt()"
297297
matTooltip="Add response to prompt messages">
298298
<mat-icon class="mr-2">add</mat-icon>
299299
Add to prompt
300300
</button>
301-
<button *ngIf="generationResponse()" mat-icon-button matTooltip="Copy Response">
301+
<button *ngIf="generationResult()" mat-icon-button matTooltip="Copy Response">
302302
<mat-icon>content_copy</mat-icon>
303303
</button>
304304
</div>
305305
</div>
306306

307+
<!-- Add this block -->
308+
<div *ngIf="generationResult() as result" class="mb-3 text-sm">
309+
<strong>Request Time:</strong> {{ result.createdAt | date : 'medium' }}. &nbsp;&nbsp;
310+
<strong>Total Time:</strong> {{ ((result.totalTime ?? 0) / 1000).toFixed(1) }}s&nbsp;&nbsp;
311+
<strong>Tokens in/out:</strong> {{ result.inputTokens }}/{{ result.outputTokens }}&nbsp;&nbsp;
312+
<strong>Cost: </strong> ${{ result.cost?.toFixed(4) }}&nbsp;&nbsp;
313+
<strong>Tok/S:</strong> {{ (result.outputTokens > 0 && result.totalTime > 0 ? (result.outputTokens / (result.totalTime / 1000)) : 0).toFixed(1) }}
314+
</div>
315+
307316
<!-- Loading State -->
308317
<div *ngIf="isGenerating()" class="flex flex-col items-center justify-center p-8">
309318
<mat-spinner diameter="40"></mat-spinner>
@@ -316,14 +325,27 @@ <h2 class="text-xl font-semibold">Response</h2>
316325
</div>
317326

318327
<!-- Response Content -->
319-
<div *ngIf="generationResponse() && !isGenerating()" class="prose max-w-none">
328+
<div *ngIf="generationResult() as result" class="prose max-w-none">
329+
@let content = result.generatedMessage.content;
330+
<!-- Add this block -->
331+
@if (getReasoningPart(content); as reasoning) {
332+
<mat-expansion-panel class="mb-3 !shadow-sm border">
333+
<mat-expansion-panel-header>
334+
<mat-panel-title class="!text-sm !font-semibold">
335+
Reasoning
336+
</mat-panel-title>
337+
</mat-expansion-panel-header>
338+
<div class="whitespace-pre-wrap break-words p-4">{{ reasoning.text }}</div>
339+
</mat-expansion-panel>
340+
}
341+
@let displayContent = getNonReasoningParts(content);
320342
<!-- Handle string response for backward compatibility and text-only responses -->
321-
<ng-container *ngIf="isString(generationResponse())">
322-
<pre class="whitespace-pre-wrap">{{ generationResponse() }}</pre>
343+
<ng-container *ngIf="isString(displayContent)">
344+
<pre class="whitespace-pre-wrap">{{ displayContent }}</pre>
323345
</ng-container>
324346
<!-- Handle complex array response (with text, images, etc.) -->
325-
<ng-container *ngIf="isArray(generationResponse())">
326-
<div *ngFor="let part of generationResponse()">
347+
<ng-container *ngIf="isArray(displayContent)">
348+
<div *ngFor="let part of displayContent">
327349
<pre *ngIf="part.type === 'text'" class="whitespace-pre-wrap">{{ part.text }}</pre>
328350
<img *ngIf="part.type === 'image'" [src]="getImageUrl(part)" alt="Generated Image" class="max-w-full h-auto rounded-md my-2">
329351
<!-- NOTE: Other part types like 'file', 'tool-call' are not visually rendered here but could be added. -->
@@ -332,7 +354,7 @@ <h2 class="text-xl font-semibold">Response</h2>
332354
</div>
333355

334356
<!-- Placeholder -->
335-
<div *ngIf="!generationResponse() && !isGenerating() && !generationError()" class="text-center text-gray-500 py-8">
357+
<div *ngIf="!generationResult() && !isGenerating() && !generationError()" class="text-center text-gray-500 py-8">
336358
<mat-icon class="text-4xl mb-2 opacity-50">bolt</mat-icon>
337359
<p>Click Generate to see a response</p>
338360
</div>

frontend/src/app/modules/prompts/form/prompt-form.component.ts

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,18 @@ import { MatSliderModule } from '@angular/material/slider';
3131
import { MatToolbarModule } from '@angular/material/toolbar';
3232
import { MatTooltipModule } from '@angular/material/tooltip';
3333
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
34-
import type { CallSettings, FilePartExt, ImagePartExt, LlmInfo, LlmMessage, TextPart, UserContentExt, AssistantContentExt } from '#shared/llm/llm.model';
34+
import type {
35+
AssistantContentExt,
36+
CallSettings,
37+
FilePartExt,
38+
ImagePartExt,
39+
LlmInfo,
40+
LlmMessage,
41+
ReasoningPart,
42+
TextPart,
43+
ToolCallPartExt,
44+
UserContentExt,
45+
} from '#shared/llm/llm.model';
3546
import type { Prompt } from '#shared/prompts/prompts.model';
3647
import { type PromptCreatePayload, PromptGenerateResponseSchemaModel, type PromptSchemaModel, type PromptUpdatePayload } from '#shared/prompts/prompts.schema';
3748
import { LlmService } from '../../llm.service';
@@ -98,7 +109,7 @@ export class PromptFormComponent implements OnInit, OnDestroy {
98109
isLoading = signal(true);
99110
isSaving = signal(false);
100111
isGenerating = signal(false);
101-
generationResponse = signal<AssistantContentExt | null>(null);
112+
generationResult = signal<PromptGenerateResponseSchemaModel | null>(null);
102113
generationError = signal<string | null>(null);
103114
private destroy$ = new Subject<void>();
104115
private llmsState$ = toObservable(this.llmService.llmsState);
@@ -418,13 +429,13 @@ export class PromptFormComponent implements OnInit, OnDestroy {
418429
this.cdr.detectChanges();
419430
}
420431

421-
private _convertLlmContentToString(content: UserContentExt | AssistantContentExt | undefined): string {
432+
private _convertLlmContentToString(content: LlmMessage['content'] | undefined): string {
422433
if (typeof content === 'string') {
423434
return content;
424435
}
425436
if (Array.isArray(content)) {
426437
return content
427-
.map((part) => {
438+
.map((part: any) => {
428439
if (part.type === 'text') {
429440
return (part as TextPart).text;
430441
}
@@ -436,6 +447,19 @@ export class PromptFormComponent implements OnInit, OnDestroy {
436447
const filePart = part as FilePartExt;
437448
return `[File: ${filePart.filename || filePart.mediaType || 'file'}]`;
438449
}
450+
if (part.type === 'reasoning') {
451+
return (part as ReasoningPart).text;
452+
}
453+
if (part.type === 'redacted-reasoning') {
454+
return '[Redacted Reasoning]';
455+
}
456+
if (part.type === 'tool-call') {
457+
const toolCallPart = part as ToolCallPartExt;
458+
return `[Tool Call: ${toolCallPart.toolName}(${JSON.stringify(toolCallPart.input)})]`;
459+
}
460+
if (part.type === 'tool-result') {
461+
return `[Tool Result: ${JSON.stringify(part.output)}]`;
462+
}
439463
// Fallback for any other part types that might appear in UserContentExt if extended
440464
// Safely access .type, provide a default if it's not a known structure
441465
const partType = (part as any)?.type || 'unknown';
@@ -630,6 +654,7 @@ export class PromptFormComponent implements OnInit, OnDestroy {
630654

631655
const generationOptions: CallSettings & { llmId?: string } = formValue.options;
632656

657+
this.generationResult.set(null);
633658
this.isGenerating.set(true);
634659
this.generationError.set(null);
635660

@@ -644,7 +669,7 @@ export class PromptFormComponent implements OnInit, OnDestroy {
644669
)
645670
.subscribe({
646671
next: (response) => {
647-
this.generationResponse.set(response.generatedMessage.content as AssistantContentExt);
672+
this.generationResult.set(response);
648673
this.cdr.detectChanges();
649674
},
650675
error: (error) => {
@@ -655,7 +680,7 @@ export class PromptFormComponent implements OnInit, OnDestroy {
655680
}
656681

657682
addResponseToPrompt(): void {
658-
const responseContent = this.generationResponse();
683+
const responseContent = this.generationResult()?.generatedMessage.content;
659684
if (!responseContent) {
660685
console.warn('No generated response to add');
661686
return;
@@ -668,7 +693,7 @@ export class PromptFormComponent implements OnInit, OnDestroy {
668693
messageGroup.get('fullContent')?.setValue(responseContent);
669694
this.messagesFormArray.push(messageGroup);
670695

671-
this.generationResponse.set(null);
696+
this.generationResult.set(null);
672697
this.generationError.set(null);
673698

674699
this.cdr.detectChanges();
@@ -826,4 +851,18 @@ export class PromptFormComponent implements OnInit, OnDestroy {
826851
}
827852
}
828853
}
854+
855+
public getReasoningPart(content: AssistantContentExt): ReasoningPart | undefined {
856+
if (Array.isArray(content)) {
857+
return content.find((part) => part.type === 'reasoning') as ReasoningPart | undefined;
858+
}
859+
return undefined;
860+
}
861+
862+
public getNonReasoningParts(content: AssistantContentExt): AssistantContentExt {
863+
if (Array.isArray(content)) {
864+
return content.filter((part) => part.type !== 'reasoning');
865+
}
866+
return content;
867+
}
829868
}

package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,16 +44,16 @@
4444
"start": " node -r ts-node/register --env-file=variables/.env src/index.ts",
4545
"start:local": "node -r ts-node/register --env-file=variables/local.env --inspect=0.0.0.0:9229 src/index.ts",
4646
"emulators": "gcloud emulators firestore start --host-port=127.0.0.1:8243",
47-
"test": " npm run test:unit && npm run test:db",
47+
"test": " pnpm run test:unit && pnpm run test:db",
4848
"test:unit": " node --env-file=variables/test.env ./node_modules/mocha/bin/mocha -r esbuild-register -r \"./src/test/testSetup.ts\" \"src/**/*.test.[jt]s\" --exclude \"src/modules/{firestore,mongo,postgres}/*.test.ts\" --timeout 10000",
4949
"test:firestore": "node --env-file=variables/test.env ./node_modules/mocha/bin/mocha -r esbuild-register -r \"./src/test/testSetup.ts\" \"src/modules/firestore/*.test.ts\" --timeout 10000",
5050
"test:postgres": " node --env-file=variables/test.env ./node_modules/mocha/bin/mocha -r esbuild-register -r \"./src/test/testSetup.ts\" \"src/modules/postgres/*.test.ts\" --timeout 10000",
5151
"test:mongo": " node --env-file=variables/test.env ./node_modules/mocha/bin/mocha -r esbuild-register -r \"./src/test/testSetup.ts\" \"src/modules/mongo/*.test.ts\" --timeout 10000",
5252
"test:db": " node --env-file=variables/test.env ./node_modules/mocha/bin/mocha -r esbuild-register -r \"./src/test/testSetup.ts\" \"src/modules/{firestore,mongo,postgres}/*.test.ts\" --timeout 10000",
5353
"test:single": " node --env-file=variables/test.env ./node_modules/mocha/bin/mocha -r esbuild-register -r \"./src/test/testSetup.ts\" --timeout 10000 --exit",
54-
"test:ci:firestore": "firebase emulators:exec --only firestore \"npm run test:firestore\"",
55-
"test:ci:postgres": " npm run test:postgres",
56-
"test:ci:mongo": " npm run test:mongo",
54+
"test:ci:firestore": "firebase emulators:exec --only firestore \"pnpm run test:firestore\"",
55+
"test:ci:postgres": " pnpm run test:postgres",
56+
"test:ci:mongo": " pnpm run test:mongo",
5757
"test:integration": "node --env-file=variables/test.env ./node_modules/mocha/bin/mocha -r ts-node/register -r \"./src/test/testSetup.ts\" \"src/**/*.int.[jt]s\" --timeout 0 --exit",
5858
"test:system": " node --env-file=variables/test.env ./node_modules/mocha/bin/mocha -r ts-node/register \"src/**/*.sys.ts\" --timeout 0 --exit",
5959
"test:unit:dev": "TS_NODE_PROJECT='./tsconfig.swc.json' node --env-file=variables/test.env ./node_modules/mocha/bin/mocha -r ts-node/register -t 0 --exit --colors \"src/**/*.test.[jt]s\"",

src/agent/agentSerialization.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export function serializeContext(context: AgentContext): AgentContextApi {
3434
codeTaskId: context.codeTaskId,
3535
state: context.state ?? 'error',
3636
callStack: context.callStack ?? [],
37-
error: context.error ?? undefined,
37+
error: context.error || undefined,
3838
output: context.output,
3939
hilBudget: context.hilBudget ?? 0,
4040
cost: context.cost ?? 0,

src/agent/autonomous/functions/agentFunctions.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export const AGENT_COMPLETED_PARAM_NAME = 'note';
1717
export class Agent {
1818
/**
1919
* Notifies that the user request has completed and there is no more work to be done, or that no more useful progress can be made with the functions.
20-
* @param {string} note A detailed description that answers/completes the user request.
20+
* @param {string} note A detailed description that answers/completes the user request using Markdown formatting.
2121
*/
2222
@func()
2323
async completed(note: string): Promise<void> {
@@ -60,7 +60,7 @@ export class Agent {
6060
async getMemory(key: string): Promise<string> {
6161
if (!key) throw new Error(`Parameter "key" must be provided. Was ${key}`);
6262
const memory = agentContext().memory;
63-
if (!memory[key]) throw new Error(`Memory key ${key} does not exist`);
63+
if (!memory[key]) throw new Error(`Memory key ${key} does not exist. Valid keys are ${Object.keys(memory).join(', ')}`);
6464
return memory[key];
6565
}
6666

src/cli/functionResolver.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ import { functionRegistry } from '../functionRegistry';
2525
const functionAliases: Record<string, string> = {
2626
f: AgentFeedback.name,
2727
swe: SoftwareDeveloperAgent.name,
28+
bash: CommandLineInterface.name,
29+
shell: CommandLineInterface.name,
2830
cli: CommandLineInterface.name,
2931
code: CodeEditingAgent.name,
3032
query: CodeFunctions.name,

src/cli/terminal.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// file: write-terminal.mjs
2+
import { createWriteStream } from 'node:fs';
3+
import { platform } from 'node:process';
4+
5+
/**
6+
* Writes a message directly to the controlling terminal, bypassing stdout/stderr redirection.
7+
* This is useful for progress indicators, password prompts, or critical alerts
8+
* that should always be visible to the user, even if they pipe the script's output to a file.
9+
*
10+
* @param {string} message The message to write to the terminal.
11+
*/
12+
export function terminalLog(message: string): void {
13+
// Determine the correct path to the terminal device based on the OS.
14+
const terminalPath = platform === 'win32' ? '\\\\.\\CON' : '/dev/tty';
15+
16+
try {
17+
// Create a writable stream to the terminal device.
18+
// This will fail if the process is not running in an interactive terminal
19+
// (e.g., in a CI/CD pipeline, a cron job, or a non-interactive SSH session).
20+
const terminalStream = createWriteStream(terminalPath, {
21+
flags: 'a', // 'a' for append mode is safest
22+
});
23+
24+
terminalStream.write(`${message}\n`);
25+
terminalStream.end(); // Close the stream to release the file handle.
26+
} catch (error) {
27+
// If we can't write to the terminal (e.g., not in a TTY),
28+
// we can fall back to stderr as a last resort for visibility.
29+
// console.error(`Fallback: Could not write directly to terminal. Message: ${message}`);
30+
}
31+
}

src/functions/commandLine.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,17 @@ export class CommandLineInterface {
1818
Current directory: ${fss.getWorkingDirectory()}
1919
Git repo folder: ${fss.getVcsRoot() ?? '<none>'}
2020
`;
21-
const response = await llms().medium.generateText([
22-
system(
23-
'You are to analyze the provided shell command to determine if it is safe to run, i.e. will not cause configuration changes, data loss or other unintended consequences to the host system or remote systems. Reading files/config and modifying files under version control is acceptable.',
24-
),
25-
user(
26-
`The command which is being requested to execute is:\n${command}\n\n\n Think through the dangers of running this command and response with only a single word, either SAFE, UNSURE or DANGEROUS`,
27-
),
28-
]);
21+
const response = await llms().medium.generateText(
22+
[
23+
system(
24+
'You are to analyze the provided shell command to determine if it is safe to run, i.e. will not cause configuration changes, data loss or other unintended consequences to the host system or remote systems. Reading files/config and modifying files under version control is acceptable.',
25+
),
26+
user(
27+
`The command which is being requested to execute is:\n${command}\n\n\n Think through the dangers of running this command and response with only a single word, either SAFE, UNSURE or DANGEROUS`,
28+
),
29+
],
30+
{ id: 'CLI command safety analysis' },
31+
);
2932
if (!response.includes('SAFE')) {
3033
await humanInTheLoop(
3134
agentContext(),

0 commit comments

Comments
 (0)