@gajae-code/coding-agent 0.7.0 → 0.7.1

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.
@@ -142,6 +142,11 @@ interface SessionRuntime {
142
142
  busy: boolean;
143
143
  /** Inbound Telegram update ids injected but not yet consumed by a turn. */
144
144
  pendingInbound: Set<number>;
145
+ /** Latest assistant text of the in-flight turn (from message_update). */
146
+ currentTurnText?: string;
147
+ /** Assistant text already flushed before an ask this turn (turn-scoped dedupe
148
+ * so turn_end does not re-emit the pre-ask lead-in). Reset each turn. */
149
+ preAskFlushedText?: string;
145
150
  }
146
151
 
147
152
  interface ResolvedSettings {
@@ -620,18 +625,42 @@ export const createNotificationsExtension: ExtensionFactory = api => {
620
625
  // Redaction suppresses streamed content (only the one-time identity header
621
626
  // survives redaction). The daemon coalesces/throttles these via its shared
622
627
  // rate-limit pool before sending to Telegram.
623
- api.on("turn_end", (event, ctx) => {
624
- const id = sessionId(ctx);
625
- const rt = runtimes.get(id);
626
- if (!rt) return;
627
- if (rt.redact) return;
628
- const text = summaryFromMessage(event.message, 3500);
629
- if (!text) return;
628
+ // Push the in-flight turn's assistant text as a finalized turn_stream, deduped
629
+ // against what was already flushed for this turn (the pre-ask lead-in).
630
+ const flushTurnText = (rt: SessionRuntime, id: string, text: string | undefined): void => {
631
+ if (!text || text === rt.preAskFlushedText) return;
632
+ rt.preAskFlushedText = text;
630
633
  try {
631
634
  rt.server.pushFrame(JSON.stringify({ type: "turn_stream", sessionId: id, phase: "finalized", text }));
632
635
  } catch (e) {
633
636
  logger.warn(`notifications: pushFrame (turn) failed: ${String(e)}`);
634
637
  }
638
+ };
639
+
640
+ // Emit the assistant text that precedes an ask BEFORE the ask's action_needed
641
+ // is broadcast, so the remote (e.g. Telegram) shows the lead-in first instead
642
+ // of only after the ask resolves at turn_end. The text is captured on
643
+ // message_end (which, like tool_execution_start, is on the awaited extension
644
+ // path and ordered before it — unlike message_update, which is queued async),
645
+ // then flushed here before the ask tool's execute calls registerAsk.
646
+ api.on("tool_execution_start", (event, ctx) => {
647
+ if (event.toolName !== "ask") return;
648
+ const id = sessionId(ctx);
649
+ const rt = runtimes.get(id);
650
+ if (!rt || rt.redact) return;
651
+ flushTurnText(rt, id, rt.currentTurnText);
652
+ });
653
+
654
+ api.on("turn_end", (event, ctx) => {
655
+ const id = sessionId(ctx);
656
+ const rt = runtimes.get(id);
657
+ if (!rt) return;
658
+ const text = rt.redact ? undefined : summaryFromMessage(event.message, 3500);
659
+ if (text) flushTurnText(rt, id, text);
660
+ // Reset per-turn streaming state so the next turn starts fresh and a later
661
+ // turn with identical text is not falsely deduped.
662
+ rt.currentTurnText = undefined;
663
+ rt.preAskFlushedText = undefined;
635
664
  });
636
665
 
637
666
  // Stream agent-produced images (computer/browser/tool screenshots) as
@@ -640,6 +669,14 @@ export const createNotificationsExtension: ExtensionFactory = api => {
640
669
  const id = sessionId(ctx);
641
670
  const rt = runtimes.get(id);
642
671
  if (!rt || rt.redact) return;
672
+ // Capture the in-flight ASSISTANT text here (message_end is on the awaited
673
+ // extension path and ordered before tool_execution_start) so the pre-ask
674
+ // flush can emit it before the ask prompt. Role-scoped: message_end also
675
+ // fires for the user prompt, which must never be mirrored back as turn output.
676
+ if ((event.message as { role?: unknown }).role === "assistant") {
677
+ const turnText = summaryFromMessage(event.message, 3500);
678
+ if (turnText) rt.currentTurnText = turnText;
679
+ }
643
680
  for (const img of imageAttachmentsFromMessage(event.message)) {
644
681
  try {
645
682
  rt.server.pushFrame(
package/src/tools/ask.ts CHANGED
@@ -675,8 +675,9 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
675
675
  }
676
676
  try {
677
677
  const deepInterviewPrompt = formatDeepInterviewSelectorPrompt(q.question);
678
+ const isDeepInterviewQuestion = deepInterviewPrompt !== null || q.deepInterview !== undefined;
678
679
  const displayQuestion = deepInterviewPrompt ?? q.question;
679
- const shouldNumberOptions = isDeepInterviewAskQuestion(q.question);
680
+ const shouldNumberOptions = isDeepInterviewQuestion || isDeepInterviewAskQuestion(q.question);
680
681
  const optionLabels = shouldNumberOptions ? numberOptionLabels(rawOptionLabels) : rawOptionLabels;
681
682
  const initialSelection =
682
683
  shouldNumberOptions && options?.previous
@@ -700,7 +701,7 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
700
701
  signal,
701
702
  initialSelection,
702
703
  navigation: options?.navigation,
703
- scrollTitleRows: deepInterviewPrompt === null ? undefined : DEEP_INTERVIEW_SELECTOR_SCROLL_TITLE_ROWS,
704
+ scrollTitleRows: isDeepInterviewQuestion ? DEEP_INTERVIEW_SELECTOR_SCROLL_TITLE_ROWS : undefined,
704
705
  otherOptionLabel: shouldNumberOptions
705
706
  ? formatNumberedOptionLabel(OTHER_OPTION, optionLabels.length)
706
707
  : undefined,