@pensar/apex 0.0.83-canary.623a32d3 → 0.0.83

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/build/index.js +455 -54
  2. package/package.json +1 -1
package/build/index.js CHANGED
@@ -31956,7 +31956,7 @@ var package_default2;
31956
31956
  var init_package = __esm(() => {
31957
31957
  package_default2 = {
31958
31958
  name: "@pensar/apex",
31959
- version: "0.0.83-canary.623a32d3",
31959
+ version: "0.0.83",
31960
31960
  description: "AI-powered penetration testing CLI tool with terminal UI",
31961
31961
  module: "src/tui/index.tsx",
31962
31962
  main: "build/index.js",
@@ -84374,6 +84374,47 @@ function parseAnthropicResponse(response, usage) {
84374
84374
  return { content, finishReason, usage: finalUsage };
84375
84375
  }
84376
84376
 
84377
+ // src/core/ai/providers/pensarSSE.ts
84378
+ async function* parseSSE(stream) {
84379
+ const reader = stream.getReader();
84380
+ const decoder2 = new TextDecoder;
84381
+ let buffer = "";
84382
+ let currentEvent = "message";
84383
+ let currentData = [];
84384
+ try {
84385
+ while (true) {
84386
+ const { done, value } = await reader.read();
84387
+ if (done)
84388
+ break;
84389
+ buffer += decoder2.decode(value, { stream: true });
84390
+ const lines = buffer.split(`
84391
+ `);
84392
+ buffer = lines.pop() ?? "";
84393
+ for (let line of lines) {
84394
+ line = line.replace(/\r$/, "");
84395
+ if (line === "") {
84396
+ if (currentData.length > 0) {
84397
+ yield { event: currentEvent, data: currentData.join(`
84398
+ `) };
84399
+ }
84400
+ currentEvent = "message";
84401
+ currentData = [];
84402
+ } else if (line.startsWith("event:")) {
84403
+ currentEvent = line.slice(6).trim();
84404
+ } else if (line.startsWith("data:")) {
84405
+ currentData.push(line.slice(5).trim());
84406
+ }
84407
+ }
84408
+ }
84409
+ if (currentData.length > 0) {
84410
+ yield { event: currentEvent, data: currentData.join(`
84411
+ `) };
84412
+ }
84413
+ } finally {
84414
+ reader.releaseLock();
84415
+ }
84416
+ }
84417
+
84377
84418
  // src/core/ai/providers/pensar.ts
84378
84419
  function log(...args) {
84379
84420
  if (DEBUG)
@@ -84472,63 +84513,248 @@ function createPensarModel(bedrockModelId, config2) {
84472
84513
  };
84473
84514
  },
84474
84515
  async doStream(options) {
84475
- log(`doStream wrapping doGenerate for ${bedrockModelId}`);
84476
- const generateResult = await model.doGenerate(options);
84477
- const parts = [];
84478
- parts.push({
84479
- type: "stream-start",
84480
- warnings: generateResult.warnings
84481
- });
84482
- for (const item of generateResult.content) {
84483
- if (item.type === "text") {
84484
- const id = `text-${Date.now()}`;
84485
- parts.push({ type: "text-start", id });
84486
- parts.push({ type: "text-delta", id, delta: item.text });
84487
- parts.push({ type: "text-end", id });
84488
- } else if (item.type === "tool-call") {
84489
- const id = item.toolCallId;
84490
- parts.push({
84491
- type: "tool-input-start",
84492
- id,
84493
- toolName: item.toolName
84494
- });
84495
- parts.push({
84496
- type: "tool-input-delta",
84497
- id,
84498
- delta: item.input
84499
- });
84500
- parts.push({ type: "tool-input-end", id });
84501
- parts.push({
84502
- type: "tool-call",
84503
- toolCallId: id,
84504
- toolName: item.toolName,
84505
- input: item.input
84506
- });
84516
+ const body = convertToBedrockFormat(bedrockModelId, options);
84517
+ const url2 = `${config2.baseUrl}/bedrock/stream`;
84518
+ log(`doStream SSE streaming for ${bedrockModelId}`);
84519
+ log(` URL: ${url2}`);
84520
+ const headers = await buildHeaders();
84521
+ let response;
84522
+ try {
84523
+ response = await fetch(url2, {
84524
+ method: "POST",
84525
+ headers,
84526
+ signal: options.abortSignal,
84527
+ body: JSON.stringify({
84528
+ modelId: bedrockModelId,
84529
+ body
84530
+ })
84531
+ });
84532
+ } catch (err) {
84533
+ logError(` SSE fetch failed, falling back to doGenerate:`, err);
84534
+ return doStreamFallback(model, options);
84535
+ }
84536
+ if (!response.ok) {
84537
+ const errorBody = await response.text();
84538
+ let errorMessage;
84539
+ try {
84540
+ const parsed = JSON.parse(errorBody);
84541
+ errorMessage = parsed.error || parsed.message || errorBody;
84542
+ } catch {
84543
+ errorMessage = errorBody;
84544
+ }
84545
+ logError(` SSE FAILED ${response.status}: ${errorMessage}`);
84546
+ if (response.status === 402) {
84547
+ throw new Error(`Insufficient Pensar credits: ${errorMessage}. ` + `Top up at https://console.pensar.dev`);
84507
84548
  }
84549
+ throw new Error(`Pensar streaming error (${response.status}): ${errorMessage}`);
84508
84550
  }
84509
- parts.push({
84510
- type: "finish",
84511
- finishReason: generateResult.finishReason,
84512
- usage: generateResult.usage
84513
- });
84551
+ if (!response.body) {
84552
+ logError(` SSE response has no body, falling back to doGenerate`);
84553
+ return doStreamFallback(model, options);
84554
+ }
84555
+ const sseStream = response.body;
84514
84556
  const stream = new ReadableStream({
84515
- start(controller) {
84516
- for (const part of parts) {
84517
- controller.enqueue(part);
84557
+ async start(controller) {
84558
+ let inputTokens = 0;
84559
+ let outputTokens = 0;
84560
+ let finishReason = "unknown";
84561
+ let startEmitted = false;
84562
+ let activeTextId = null;
84563
+ let activeToolId = null;
84564
+ let activeToolName = null;
84565
+ let activeToolInput = "";
84566
+ try {
84567
+ for await (const sse of parseSSE(sseStream)) {
84568
+ let parsed;
84569
+ try {
84570
+ parsed = JSON.parse(sse.data);
84571
+ } catch {
84572
+ continue;
84573
+ }
84574
+ if (sse.event === "error") {
84575
+ const msg = parsed.error ?? "Unknown streaming error";
84576
+ controller.error(new Error(msg));
84577
+ return;
84578
+ }
84579
+ if (sse.event === "pensar:usage") {
84580
+ if (parsed.totalCost != null) {
84581
+ log(` cost: $${Number(parsed.totalCost).toFixed(6)}`);
84582
+ }
84583
+ continue;
84584
+ }
84585
+ const type = parsed.type;
84586
+ switch (type) {
84587
+ case "message_start": {
84588
+ if (!startEmitted) {
84589
+ controller.enqueue({ type: "stream-start", warnings: [] });
84590
+ startEmitted = true;
84591
+ }
84592
+ const msg = parsed.message;
84593
+ const usage = msg?.usage;
84594
+ if (usage?.input_tokens) {
84595
+ inputTokens = usage.input_tokens;
84596
+ }
84597
+ break;
84598
+ }
84599
+ case "content_block_start": {
84600
+ if (!startEmitted) {
84601
+ controller.enqueue({ type: "stream-start", warnings: [] });
84602
+ startEmitted = true;
84603
+ }
84604
+ const cb = parsed.content_block;
84605
+ const cbType = cb?.type;
84606
+ if (cbType === "text") {
84607
+ activeTextId = `text-${Date.now()}-${parsed.index}`;
84608
+ controller.enqueue({
84609
+ type: "text-start",
84610
+ id: activeTextId
84611
+ });
84612
+ } else if (cbType === "tool_use") {
84613
+ activeToolId = cb?.id ?? `tool-${Date.now()}`;
84614
+ activeToolName = cb?.name ?? "unknown";
84615
+ activeToolInput = "";
84616
+ controller.enqueue({
84617
+ type: "tool-input-start",
84618
+ id: activeToolId,
84619
+ toolName: activeToolName
84620
+ });
84621
+ }
84622
+ break;
84623
+ }
84624
+ case "content_block_delta": {
84625
+ const delta = parsed.delta;
84626
+ const deltaType = delta?.type;
84627
+ if (deltaType === "text_delta" && activeTextId) {
84628
+ controller.enqueue({
84629
+ type: "text-delta",
84630
+ id: activeTextId,
84631
+ delta: delta?.text ?? ""
84632
+ });
84633
+ } else if (deltaType === "input_json_delta" && activeToolId) {
84634
+ const jsonDelta = delta?.partial_json ?? "";
84635
+ activeToolInput += jsonDelta;
84636
+ controller.enqueue({
84637
+ type: "tool-input-delta",
84638
+ id: activeToolId,
84639
+ delta: jsonDelta
84640
+ });
84641
+ }
84642
+ break;
84643
+ }
84644
+ case "content_block_stop": {
84645
+ if (activeTextId) {
84646
+ controller.enqueue({ type: "text-end", id: activeTextId });
84647
+ activeTextId = null;
84648
+ } else if (activeToolId && activeToolName) {
84649
+ controller.enqueue({
84650
+ type: "tool-input-end",
84651
+ id: activeToolId
84652
+ });
84653
+ controller.enqueue({
84654
+ type: "tool-call",
84655
+ toolCallId: activeToolId,
84656
+ toolName: activeToolName,
84657
+ input: activeToolInput
84658
+ });
84659
+ activeToolId = null;
84660
+ activeToolName = null;
84661
+ activeToolInput = "";
84662
+ }
84663
+ break;
84664
+ }
84665
+ case "message_delta": {
84666
+ const delta = parsed.delta;
84667
+ const stopReason = delta?.stop_reason;
84668
+ if (stopReason) {
84669
+ finishReason = mapStopReason(stopReason);
84670
+ }
84671
+ const usage = parsed.usage;
84672
+ if (usage?.output_tokens) {
84673
+ outputTokens = usage.output_tokens;
84674
+ }
84675
+ break;
84676
+ }
84677
+ case "message_stop": {
84678
+ break;
84679
+ }
84680
+ }
84681
+ }
84682
+ log(` stream done: ${finishReason}, ${inputTokens}in/${outputTokens}out`);
84683
+ controller.enqueue({
84684
+ type: "finish",
84685
+ finishReason,
84686
+ usage: {
84687
+ inputTokens,
84688
+ outputTokens,
84689
+ totalTokens: inputTokens + outputTokens
84690
+ }
84691
+ });
84692
+ controller.close();
84693
+ } catch (err) {
84694
+ controller.error(err);
84518
84695
  }
84519
- controller.close();
84520
84696
  }
84521
84697
  });
84522
84698
  return {
84523
84699
  stream,
84524
- request: {
84525
- body: generateResult.request?.body
84526
- }
84700
+ request: { body }
84527
84701
  };
84528
84702
  }
84529
84703
  };
84530
84704
  return model;
84531
84705
  }
84706
+ function mapStopReason(reason) {
84707
+ switch (reason) {
84708
+ case "end_turn":
84709
+ return "stop";
84710
+ case "max_tokens":
84711
+ return "length";
84712
+ case "tool_use":
84713
+ return "tool-calls";
84714
+ case "stop_sequence":
84715
+ return "stop";
84716
+ default:
84717
+ return "unknown";
84718
+ }
84719
+ }
84720
+ async function doStreamFallback(model, options) {
84721
+ const generateResult = await model.doGenerate(options);
84722
+ const parts = [];
84723
+ parts.push({ type: "stream-start", warnings: generateResult.warnings });
84724
+ for (const item of generateResult.content) {
84725
+ if (item.type === "text") {
84726
+ const id = `text-${Date.now()}`;
84727
+ parts.push({ type: "text-start", id });
84728
+ parts.push({ type: "text-delta", id, delta: item.text });
84729
+ parts.push({ type: "text-end", id });
84730
+ } else if (item.type === "tool-call") {
84731
+ const id = item.toolCallId;
84732
+ parts.push({ type: "tool-input-start", id, toolName: item.toolName });
84733
+ parts.push({ type: "tool-input-delta", id, delta: item.input });
84734
+ parts.push({ type: "tool-input-end", id });
84735
+ parts.push({
84736
+ type: "tool-call",
84737
+ toolCallId: id,
84738
+ toolName: item.toolName,
84739
+ input: item.input
84740
+ });
84741
+ }
84742
+ }
84743
+ parts.push({
84744
+ type: "finish",
84745
+ finishReason: generateResult.finishReason,
84746
+ usage: generateResult.usage
84747
+ });
84748
+ const stream = new ReadableStream({
84749
+ start(controller) {
84750
+ for (const part of parts) {
84751
+ controller.enqueue(part);
84752
+ }
84753
+ controller.close();
84754
+ }
84755
+ });
84756
+ return { stream, request: { body: generateResult.request?.body } };
84757
+ }
84532
84758
  var DEBUG;
84533
84759
  var init_pensar2 = __esm(() => {
84534
84760
  DEBUG = process.env.PENSAR_DEBUG === "1" || process.env.PENSAR_DEBUG === "true";
@@ -107948,6 +108174,11 @@ They are organised into categories:
107948
108174
  - "framework" — framework-specific notes (e.g. Rails tricks, Django patterns)
107949
108175
  - "general" — catch-all (default when category is omitted)
107950
108176
 
108177
+ IMPORTANT: Before adding a new memory, always use list_memories first to check
108178
+ for existing memories that overlap with what you intend to save. If a relevant
108179
+ memory already exists, update it (delete + re-add) rather than creating a
108180
+ duplicate. This keeps the memory store clean and avoids conflicting entries.
108181
+
107951
108182
  Use this to record reusable techniques, target-specific notes, credential
107952
108183
  patterns, useful payloads, or any information worth remembering for future
107953
108184
  engagements.`,
@@ -189828,6 +190059,7 @@ You can perform the full lifecycle of a penetration test and support a wide rang
189828
190059
  4. **Use the right level of automation.** For large tasks (e.g., "pentest this entire application"), use orchestration tools like \`run_attack_surface\` and \`spawn_pentest_swarm\` to parallelize the work. For focused tasks (e.g., "check if this parameter is vulnerable to XSS"), work directly with \`http_request\`, \`execute_command\`, or the browser tools.
189829
190060
  5. **Authenticate when needed.** If the user provides credentials or auth instructions, use them. Try \`authenticate_session\` first; fall back to \`delegate_to_auth_subagent\` for complex flows (OAuth, SAML, CSRF-protected SPAs).
189830
190061
  6. **Document as you go.** Call \`document_asset\` when you discover assets. Call \`document_finding\` when you confirm vulnerabilities. Create PoCs with \`create_poc\` to demonstrate exploitability. Don't defer documentation to the end.
190062
+ 7. **Consult memories first.** When you begin testing a specific application, framework, or path, call \`list_memories\` to check for saved knowledge from previous sessions — past findings, useful payloads, endpoint patterns, or target-specific notes. Use relevant memories to inform your approach before starting from scratch.
189831
190063
 
189832
190064
  # Command Execution
189833
190065
 
@@ -270639,6 +270871,7 @@ var PromptInput = import_react29.forwardRef(function PromptInput2({
270639
270871
  enableCommands = false,
270640
270872
  onCommandExecute,
270641
270873
  commandHistory = [],
270874
+ disableHistoryNavigation = false,
270642
270875
  showPromptIndicator = false
270643
270876
  }, ref) {
270644
270877
  const { colors: colors2 } = useTheme();
@@ -270713,6 +270946,8 @@ var PromptInput = import_react29.forwardRef(function PromptInput2({
270713
270946
  }
270714
270947
  return;
270715
270948
  }
270949
+ if (disableHistoryNavigation)
270950
+ return;
270716
270951
  const history = historyRef.current;
270717
270952
  const currentState = {
270718
270953
  historyIndex,
@@ -280690,13 +280925,13 @@ function NormalInputAreaInner({
280690
280925
  autocompleteOptions = [],
280691
280926
  enableCommands = false,
280692
280927
  onCommandExecute,
280693
- commandHistory = []
280928
+ commandHistory = [],
280929
+ disableHistoryNavigation = false
280694
280930
  }) {
280695
280931
  const { colors: colors2 } = useTheme();
280696
280932
  const { inputValue, setInputValue } = useInput();
280697
280933
  const promptRef = import_react77.useRef(null);
280698
280934
  const isExternalUpdate = import_react77.useRef(false);
280699
- const isDisabled = status === "running";
280700
280935
  import_react77.useEffect(() => {
280701
280936
  if (value !== inputValue) {
280702
280937
  isExternalUpdate.current = true;
@@ -280733,7 +280968,7 @@ function NormalInputAreaInner({
280733
280968
  backgroundColor: "transparent",
280734
280969
  children: [
280735
280970
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
280736
- fg: isDisabled ? colors2.textMuted : colors2.primary,
280971
+ fg: !focused ? colors2.textMuted : colors2.primary,
280737
280972
  children: ">"
280738
280973
  }, undefined, false, undefined, this),
280739
280974
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(PromptInput, {
@@ -280742,14 +280977,15 @@ function NormalInputAreaInner({
280742
280977
  minHeight: 1,
280743
280978
  maxHeight: 3,
280744
280979
  textColor: colors2.text,
280745
- focused: focused && !isDisabled,
280746
- placeholder: isDisabled ? "Processing..." : placeholder,
280980
+ focused,
280981
+ placeholder,
280747
280982
  onSubmit: handleSubmit,
280748
280983
  enableAutocomplete,
280749
280984
  autocompleteOptions,
280750
280985
  enableCommands,
280751
280986
  onCommandExecute,
280752
- commandHistory
280987
+ commandHistory,
280988
+ disableHistoryNavigation
280753
280989
  }, undefined, false, undefined, this)
280754
280990
  ]
280755
280991
  }, undefined, true, undefined, this),
@@ -280836,6 +281072,7 @@ function InputArea(props) {
280836
281072
  enableCommands,
280837
281073
  onCommandExecute,
280838
281074
  commandHistory,
281075
+ disableHistoryNavigation,
280839
281076
  ...normalProps
280840
281077
  } = props;
280841
281078
  if (pendingApproval) {
@@ -280859,6 +281096,7 @@ function InputArea(props) {
280859
281096
  enableCommands,
280860
281097
  onCommandExecute,
280861
281098
  commandHistory,
281099
+ disableHistoryNavigation,
280862
281100
  ...normalProps
280863
281101
  }, undefined, false, undefined, this)
280864
281102
  }, undefined, false, undefined, this);
@@ -281074,6 +281312,89 @@ function accumulateTokenUsage(current, stepInputTokens, stepOutputTokens) {
281074
281312
  };
281075
281313
  }
281076
281314
 
281315
+ // src/tui/components/operator-dashboard/queued-messages.tsx
281316
+ function QueuedMessages({
281317
+ messages,
281318
+ selectedIndex
281319
+ }) {
281320
+ const { colors: colors2 } = useTheme();
281321
+ if (messages.length === 0)
281322
+ return null;
281323
+ return /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("box", {
281324
+ flexDirection: "column",
281325
+ paddingLeft: 2,
281326
+ paddingRight: 2,
281327
+ paddingBottom: 1,
281328
+ flexShrink: 0,
281329
+ children: [
281330
+ /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("box", {
281331
+ flexDirection: "row",
281332
+ gap: 1,
281333
+ marginBottom: 0,
281334
+ children: /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
281335
+ fg: colors2.textMuted,
281336
+ children: [
281337
+ "Queued (",
281338
+ messages.length,
281339
+ ")"
281340
+ ]
281341
+ }, undefined, true, undefined, this)
281342
+ }, undefined, false, undefined, this),
281343
+ messages.map((msg, index) => {
281344
+ const isSelected = index === selectedIndex;
281345
+ const displayText = msg.length > 80 ? msg.slice(0, 77) + "…" : msg;
281346
+ return /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("box", {
281347
+ flexDirection: "row",
281348
+ gap: 1,
281349
+ children: [
281350
+ /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
281351
+ fg: isSelected ? colors2.primary : colors2.textMuted,
281352
+ children: isSelected ? "▸" : " "
281353
+ }, undefined, false, undefined, this),
281354
+ /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
281355
+ fg: isSelected ? colors2.text : colors2.textMuted,
281356
+ children: displayText
281357
+ }, undefined, false, undefined, this)
281358
+ ]
281359
+ }, `q-${index}`, true, undefined, this);
281360
+ }),
281361
+ /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("box", {
281362
+ flexDirection: "row",
281363
+ gap: 2,
281364
+ marginTop: 0,
281365
+ children: /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
281366
+ fg: colors2.textMuted,
281367
+ children: "↑↓ select • Enter send now • ⌫ delete • e edit"
281368
+ }, undefined, false, undefined, this)
281369
+ }, undefined, false, undefined, this)
281370
+ ]
281371
+ }, undefined, true, undefined, this);
281372
+ }
281373
+
281374
+ // src/tui/components/operator-dashboard/queue.ts
281375
+ function selectionAfterRemove(prevQueueLength, selectedIndex) {
281376
+ const newLen = prevQueueLength - 1;
281377
+ if (newLen === 0)
281378
+ return -1;
281379
+ return Math.min(selectedIndex, newLen - 1);
281380
+ }
281381
+ function navigateUp(selectedIndex, queueLength) {
281382
+ if (queueLength === 0)
281383
+ return -1;
281384
+ if (selectedIndex === -1)
281385
+ return queueLength - 1;
281386
+ if (selectedIndex === 0)
281387
+ return 0;
281388
+ return selectedIndex - 1;
281389
+ }
281390
+ function navigateDown(selectedIndex, queueLength) {
281391
+ if (selectedIndex === -1)
281392
+ return -1;
281393
+ if (selectedIndex >= queueLength - 1)
281394
+ return -1;
281395
+ return selectedIndex + 1;
281396
+ }
281397
+
281077
281398
  // src/tui/components/operator-dashboard/index.tsx
281078
281399
  import { existsSync as existsSync26, readFileSync as readFileSync13 } from "fs";
281079
281400
  import { join as join27 } from "path";
@@ -281127,6 +281448,17 @@ function OperatorDashboard({
281127
281448
  const textRef = import_react79.useRef("");
281128
281449
  const conversationRef = import_react79.useRef([]);
281129
281450
  const [inputValue, setInputValue] = import_react79.useState("");
281451
+ const [queuedMessages, setQueuedMessages] = import_react79.useState([]);
281452
+ const [selectedQueueIndex, setSelectedQueueIndex] = import_react79.useState(-1);
281453
+ const queuedMessagesRef = import_react79.useRef([]);
281454
+ import_react79.useEffect(() => {
281455
+ queuedMessagesRef.current = queuedMessages;
281456
+ if (queuedMessages.length === 0) {
281457
+ setSelectedQueueIndex(-1);
281458
+ } else if (selectedQueueIndex >= queuedMessages.length) {
281459
+ setSelectedQueueIndex(queuedMessages.length - 1);
281460
+ }
281461
+ }, [queuedMessages, selectedQueueIndex]);
281130
281462
  const [operatorState, setOperatorState] = import_react79.useState(() => createInitialOperatorState("manual", true));
281131
281463
  const approvalGateRef = import_react79.useRef(new ApprovalGate({ requireApproval: true }));
281132
281464
  const [pendingApprovals, setPendingApprovals] = import_react79.useState([]);
@@ -281616,6 +281948,11 @@ function OperatorDashboard({
281616
281948
  approvalGateRef.current.deny(p.id);
281617
281949
  }
281618
281950
  }
281951
+ if (result.action === "blocked" && value.trim()) {
281952
+ setQueuedMessages((prev) => [...prev, value.trim()]);
281953
+ setInputValue("");
281954
+ return;
281955
+ }
281619
281956
  if (result.action !== "run" || !result.prompt)
281620
281957
  return;
281621
281958
  setInputValue("");
@@ -281630,6 +281967,17 @@ function OperatorDashboard({
281630
281967
  runAgentRef.current(initialMessage);
281631
281968
  }
281632
281969
  }, [loading, initialMessage]);
281970
+ import_react79.useEffect(() => {
281971
+ if (status !== "idle")
281972
+ return;
281973
+ const queue = queuedMessagesRef.current;
281974
+ if (queue.length === 0)
281975
+ return;
281976
+ const next = queue[0];
281977
+ setQueuedMessages((prev) => prev.slice(1));
281978
+ setSelectedQueueIndex(-1);
281979
+ runAgentRef.current(next);
281980
+ }, [status]);
281633
281981
  const showModelPicker = import_react79.useCallback(() => {
281634
281982
  setDialogSize("large");
281635
281983
  showDialog(/* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("box", {
@@ -281730,6 +282078,9 @@ function OperatorDashboard({
281730
282078
  abortControllerRef.current.abort();
281731
282079
  abortControllerRef.current = null;
281732
282080
  commandCancelledRef.current = false;
282081
+ setQueuedMessages([]);
282082
+ queuedMessagesRef.current = [];
282083
+ setSelectedQueueIndex(-1);
281733
282084
  setStatus("idle");
281734
282085
  setThinking(false);
281735
282086
  setIsExecuting(false);
@@ -281765,6 +282116,51 @@ function OperatorDashboard({
281765
282116
  });
281766
282117
  }, []);
281767
282118
  useKeyboard((key) => {
282119
+ if (status === "running" && queuedMessages.length > 0 && !inputValue.trim()) {
282120
+ if (key.name === "up") {
282121
+ key.preventDefault?.();
282122
+ setSelectedQueueIndex((prev) => navigateUp(prev, queuedMessages.length));
282123
+ return;
282124
+ }
282125
+ if (selectedQueueIndex >= 0) {
282126
+ if (key.name === "down") {
282127
+ key.preventDefault?.();
282128
+ setSelectedQueueIndex((prev) => navigateDown(prev, queuedMessages.length));
282129
+ return;
282130
+ }
282131
+ if (key.name === "backspace" || key.name === "delete") {
282132
+ key.preventDefault?.();
282133
+ const removeIdx = selectedQueueIndex;
282134
+ setQueuedMessages((prev) => prev.filter((_3, i2) => i2 !== removeIdx));
282135
+ setSelectedQueueIndex(() => selectionAfterRemove(queuedMessages.length, removeIdx));
282136
+ return;
282137
+ }
282138
+ if (key.name === "return") {
282139
+ key.preventDefault?.();
282140
+ const msg = queuedMessages[selectedQueueIndex];
282141
+ const removeIdx = selectedQueueIndex;
282142
+ setQueuedMessages((prev) => prev.filter((_3, i2) => i2 !== removeIdx));
282143
+ setSelectedQueueIndex(-1);
282144
+ flushCommandOutput();
282145
+ if (cmdFlushTimerRef.current) {
282146
+ clearInterval(cmdFlushTimerRef.current);
282147
+ cmdFlushTimerRef.current = null;
282148
+ }
282149
+ setMessages((prev) => prev.map((m4) => isToolMessage(m4) && m4.status === "pending" ? { ...m4, status: "error", result: "Interrupted" } : m4));
282150
+ runAgentRef.current(msg);
282151
+ return;
282152
+ }
282153
+ if (key.raw === "e" || key.raw === "E") {
282154
+ key.preventDefault?.();
282155
+ const msg = queuedMessages[selectedQueueIndex];
282156
+ const removeIdx = selectedQueueIndex;
282157
+ setQueuedMessages((prev) => prev.filter((_3, i2) => i2 !== removeIdx));
282158
+ setInputValue(msg);
282159
+ setSelectedQueueIndex(-1);
282160
+ return;
282161
+ }
282162
+ }
282163
+ }
281768
282164
  const dialogOpen = stack.length > 0 || externalDialogOpen;
281769
282165
  const action = resolveKeyboardShortcut(key, status, inputValue, pendingApprovals.length > 0, dialogOpen);
281770
282166
  switch (action.type) {
@@ -281916,12 +282312,16 @@ function OperatorDashboard({
281916
282312
  pendingApprovals,
281917
282313
  lastApprovedAction
281918
282314
  }, undefined, false, undefined, this),
282315
+ /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(QueuedMessages, {
282316
+ messages: queuedMessages,
282317
+ selectedIndex: selectedQueueIndex
282318
+ }, undefined, false, undefined, this),
281919
282319
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(InputArea, {
281920
282320
  value: inputValue,
281921
282321
  onChange: setInputValue,
281922
282322
  onSubmit: handleSubmit,
281923
- placeholder: status === "running" ? "Agent is working..." : status === "waiting" ? "Type to redirect agent, or Y/A to approve..." : "Enter directive or / for commands & skills...",
281924
- focused: resolveInputFocused(status, stack.length, externalDialogOpen),
282323
+ placeholder: status === "running" ? "Queue a follow-up message..." : status === "waiting" ? "Type to redirect agent, or Y/A to approve..." : "Enter directive or / for commands & skills...",
282324
+ focused: status === "running" ? selectedQueueIndex < 0 : resolveInputFocused(status, stack.length, externalDialogOpen),
281925
282325
  status: status === "waiting" ? "running" : status,
281926
282326
  mode: "operator",
281927
282327
  operatorMode: operatorState.mode,
@@ -281933,7 +282333,8 @@ function OperatorDashboard({
281933
282333
  enableAutocomplete: true,
281934
282334
  autocompleteOptions,
281935
282335
  enableCommands: true,
281936
- onCommandExecute: handleCommandExecute
282336
+ onCommandExecute: handleCommandExecute,
282337
+ disableHistoryNavigation: status === "running" && queuedMessages.length > 0
281937
282338
  }, undefined, false, undefined, this)
281938
282339
  ]
281939
282340
  }, undefined, true, undefined, this);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pensar/apex",
3
- "version": "0.0.83-canary.623a32d3",
3
+ "version": "0.0.83",
4
4
  "description": "AI-powered penetration testing CLI tool with terminal UI",
5
5
  "module": "src/tui/index.tsx",
6
6
  "main": "build/index.js",