@roll-agent/core 0.20.0 → 0.21.0

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 (36) hide show
  1. package/dist/cli/chat/ink/app.d.ts +3 -3
  2. package/dist/cli/chat/ink/app.js +1 -1
  3. package/dist/cli/chat/ink/banner-view.d.ts +3 -4
  4. package/dist/cli/chat/ink/confirm-select.d.ts +3 -1
  5. package/dist/cli/chat/ink/confirm-select.js +1 -1
  6. package/dist/cli/chat/ink/layout.d.ts +13 -0
  7. package/dist/cli/chat/ink/layout.js +1 -0
  8. package/dist/cli/chat/ink/line-buffer.d.ts +6 -0
  9. package/dist/cli/chat/ink/line-buffer.js +1 -1
  10. package/dist/cli/chat/ink/mouse-input.d.ts +9 -0
  11. package/dist/cli/chat/ink/mouse-input.js +1 -0
  12. package/dist/cli/chat/ink/run-ink-repl.d.ts +2 -2
  13. package/dist/cli/chat/ink/run-ink-repl.js +1 -1
  14. package/dist/cli/chat/ink/slash-popup.d.ts +3 -1
  15. package/dist/cli/chat/ink/slash-popup.js +1 -1
  16. package/dist/cli/chat/ink/status-line.d.ts +3 -2
  17. package/dist/cli/chat/ink/status-line.js +1 -1
  18. package/dist/cli/chat/ink/terminal-output.d.ts +6 -0
  19. package/dist/cli/chat/ink/terminal-output.js +1 -0
  20. package/dist/cli/chat/ink/text-prompt.d.ts +4 -0
  21. package/dist/cli/chat/ink/text-prompt.js +1 -1
  22. package/dist/cli/chat/ink/transcript-viewport.d.ts +22 -0
  23. package/dist/cli/chat/ink/transcript-viewport.js +1 -0
  24. package/dist/cli/chat/ink/turn-status-line.d.ts +3 -2
  25. package/dist/cli/chat/ink/turn-status-line.js +1 -1
  26. package/dist/cli/chat/screen-mode.d.ts +66 -0
  27. package/dist/cli/chat/screen-mode.js +1 -0
  28. package/dist/cli/commands/browser-stop.d.ts +4 -4
  29. package/dist/cli/commands/chat.d.ts +5 -1
  30. package/dist/cli/commands/chat.js +1 -1
  31. package/dist/config/application-service.js +1 -1
  32. package/dist/config/guidance.d.ts +15 -3
  33. package/dist/config/guidance.js +1 -1
  34. package/dist/config/schema.d.ts +99 -75
  35. package/dist/config/schema.js +1 -1
  36. package/package.json +1 -1
@@ -3,18 +3,18 @@ import type { AgentSession } from "@roll-agent/runtime";
3
3
  import type { ThinkingLevel } from "../../../llm/providers.ts";
4
4
  import { type HistoryItem } from "./state.ts";
5
5
  import { type SlashSkillSummary } from "./commands.ts";
6
- import { type BannerLine } from "../banner.ts";
6
+ import { type BannerInfo } from "../banner.ts";
7
7
  export interface ChatAppProps {
8
8
  readonly session: AgentSession;
9
9
  readonly model: string;
10
10
  readonly contextWindow: number | undefined;
11
11
  readonly initialHistory?: readonly HistoryItem[];
12
- /** Banner played as an entrance animation in the dynamic region, then committed to Static. */
13
- readonly animatedBanner?: readonly BannerLine[];
12
+ readonly banner?: BannerInfo;
14
13
  readonly initialThinkingLevel?: ThinkingLevel;
15
14
  readonly availableSkills?: readonly SlashSkillSummary[];
16
15
  readonly onThinkingChange?: (level: ThinkingLevel) => void;
17
16
  readonly onUserSubmit: (text: string) => void;
18
17
  readonly onExit: () => void;
19
18
  }
19
+ export declare const INK_HINTS = "/exit \u9000\u51FA \u00B7 Esc \u4E2D\u65AD \u00B7 / \u547D\u4EE4 \u00B7 Shift+Enter/Ctrl+J \u6362\u884C \u00B7 Alt+./Alt+, \u8C03\u63A8\u7406 \u00B7 Shift+Tab \u81EA\u52A8\u6279\u51C6";
20
20
  export declare function ChatApp(props: ChatAppProps): ReactElement;
@@ -1 +1 @@
1
- import{randomUUID as i}from"node:crypto";import{createElement as t,useCallback as n,useMemo as o,useRef as e,useState as r}from"react";import{Box as s,Static as a,useInput as m}from"ink";import{useSession as l}from"./use-session.js";import{CHAT_PHASES as d}from"./state.js";import{HistoryItemView as p}from"./history-item.js";import{LiveRegion as f}from"./live-region.js";import{StatusLine as c}from"./status-line.js";import{TextPrompt as u}from"./text-prompt.js";import{ConfirmSelect as h}from"./confirm-select.js";import{SlashPopup as g}from"./slash-popup.js";import{buildSkillListLines as v,filterSlashEntries as k,parseSkillInvocation as j,SLASH_COMMANDS as y}from"./commands.js";import{bannerTextLine as b}from"../banner.js";import{BannerHistoryView as x}from"./banner-view.js";import{cycleThinking as C}from"./thinking.js";import{appendInputHistory as L}from"./input-history.js";import{resolveTurnActivity as T}from"./turn-activity.js";import{TurnStatusLine as M}from"./turn-status-line.js";function A(){return y.map(i=>`${i.name} — ${i.description}`).join("\n")}export function ChatApp(H){const{session:S,model:$,contextWindow:w,onUserSubmit:W,onExit:D}=H,B=H.availableSkills??[],[E,P]=r(()=>(H.initialHistory??[]).reduce((i,t)=>"user"===t.kind?L(i,t.text):i,[])),R=n(i=>{P(t=>L(t,i))},[]),{state:U,submit:q,compact:z,cancel:F,resolveConfirm:G,setDraft:I,setThinking:J,setAutoMode:K,toggleAutoMode:N,commitHistory:O}=l(S,{model:$,contextWindow:w,...H.initialHistory?{initialHistory:H.initialHistory}:{},...H.initialThinkingLevel?{initialThinkingLevel:H.initialThinkingLevel}:{},...H.onThinkingChange?{onThinkingChange:H.onThinkingChange}:{}}),Q=o(()=>[...U.history],[U.history]),[V,X]=r(!1),Y=e(H.animatedBanner),Z=n(()=>{const i=Y.current;void 0!==i&&(Y.current=void 0,O({kind:"banner",id:"banner",lines:i})),X(!0)},[O]),_=V?void 0:H.animatedBanner,[ii,ti]=r(0),ni=U.phase===d.idle&&U.draft.startsWith("/"),oi=ni&&!0===U.draft.split(/\s+/).at(-1)?.startsWith("/"),ei=oi?k(U.draft,B):[],ri=Math.max(ei.length-1,0),si=Math.min(ii,ri);m((i,t)=>{U.phase===d.busy&&t.escape&&!t.meta?F():t.tab&&t.shift?N():t.meta&&"."===i?J(C(U.status.thinkingLevel,1)):t.meta&&","===i&&J(C(U.status.thinkingLevel,-1))});const ai=i=>{const t=i.trim();0!==t.length?(R(t),Z(),W(t),q(t)):I("")},mi=U.phase===d.confirm&&void 0!==U.pendingConfirm?t(h,{prompt:U.pendingConfirm.prompt,args:U.pendingConfirm.args,onDecide:G}):t(u,{value:U.draft,inputHistory:E,disabled:U.phase!==d.idle,...U.phase===d.cancelling?{disabledHint:"中断请求已发送,等待当前活动退出…"}:{},slashActive:ni,slashPopupActive:oi,autoApprove:U.status.autoApprove,onChange:I,onSubmit:ai,onSlashMove:i=>{ti(t=>Math.min(Math.max(Math.min(t,ri)+i,0),ri))},onSlashComplete:()=>{const i=ei[si];if(i){const t=U.draft.split(/\s+/);t[t.length-1]=i.name,I(`${t.join(" ")} `),ti(0)}},onSlashRun:t=>{const n=t.trim().split(/\s+/,1)[0]??"",o=y.some(i=>i.name===n),e=ei[si];if(!o&&"skill"===e?.kind){const i=U.draft.split(/\s+/);return i[i.length-1]=e.name,I(`${i.join(" ")} `),void ti(0)}(t=>{Z(),I("");const n=t.trim();R(n);const o=n.split(/\s+/),e=o[0]??"",r=(o[1]??"").toLowerCase(),s=U.status.thinkingLevel;if("/compact"===e)return void z();if("/think"===e)return void J("off"===r?"off":"on"===r?"off"===s?"medium":s:"off"===s?"medium":"off");if("/effort"===e)return void("low"===r||"medium"===r||"high"===r?J(r):O({kind:"notice",id:i(),text:"用法: /effort low | medium | high"}));if("/auto"===e)return void("on"===r?K(!0):"off"===r?K(!1):N());if("/skills"===e){const t=(process.stdout.columns||80)-2,[n,...o]=v(B,t);return void O({kind:"banner",id:i(),lines:[b(n??""),...o.map(i=>b(i,{dim:!0}))]})}if("/help"===e)return void O({kind:"notice",id:i(),text:A()});if("/exit"===e)return void D();const a=j(n,B);if(a)return 0===a.prompt.length?void I(`${a.skills.map(i=>`/${i.name}`).join(" ")} `):void ai(n);O({kind:"notice",id:i(),text:`未知命令 ${e}`})})(o?t:e?.name??t)}}),li=T(U);return t(s,{flexDirection:"column"},t(a,{items:Q,children:(i,n)=>{const o=Q[n-1],e="user"===i.kind||"assistant"===i.kind||"reasoning"===i.kind||"reasoning"===o?.kind,r="tool"===i.kind||"denied"===i.kind||"cancelled"===i.kind;return t(s,{key:i.id,marginTop:e?1:0,marginLeft:r?3:1},t(p,{item:i}))}}),void 0!==_?t(s,{marginLeft:1},t(x,{lines:_,onSettled:Z})):null,t(s,{marginLeft:1},t(f,{live:U.live})),t(s,{marginTop:void 0===li?0:1},void 0===li?t(c,{status:U.status}):t(M,{activity:li})),ni?t(g,{matches:ei,selected:si}):null,mi)}
1
+ import{randomUUID as t}from"node:crypto";import{createElement as i,useCallback as o,useState as n}from"react";import{Box as e,Text as s,useInput as r,useWindowSize as m}from"ink";import{useSession as a}from"./use-session.js";import{CHAT_PHASES as l}from"./state.js";import{StatusLine as d}from"./status-line.js";import{TextPrompt as p}from"./text-prompt.js";import{ConfirmSelect as h}from"./confirm-select.js";import{SlashPopup as f}from"./slash-popup.js";import{buildSkillListLines as c,filterSlashEntries as u,parseSkillInvocation as v,SLASH_COMMANDS as g}from"./commands.js";import{bannerTextLine as w,buildBannerLines as k}from"../banner.js";import{cycleThinking as x}from"./thinking.js";import{appendInputHistory as j}from"./input-history.js";import{resolveTurnActivity as y}from"./turn-activity.js";import{TurnStatusLine as b}from"./turn-status-line.js";import{resolveChatLayout as C}from"./layout.js";import{TranscriptViewport as S}from"./transcript-viewport.js";export const INK_HINTS="/exit 退出 · Esc 中断 · / 命令 · Shift+Enter/Ctrl+J 换行 · Alt+./Alt+, 调推理 · Shift+Tab 自动批准";function H(){return g.map(t=>`${t.name} — ${t.description}`).join("\n")}export function ChatApp(R){const{session:T,model:A,contextWindow:M,onUserSubmit:L,onExit:$}=R,I=m(),W=C(I.columns,I.rows),D=R.availableSkills??[],[E,N]=n(()=>(R.initialHistory??[]).reduce((t,i)=>"user"===i.kind?j(t,i.text):t,[])),B=o(t=>{N(i=>j(i,t))},[]),{state:K,submit:_,compact:J,cancel:P,resolveConfirm:U,setDraft:q,setThinking:z,setAutoMode:F,toggleAutoMode:G,commitHistory:O}=a(T,{model:A,contextWindow:M,...R.initialHistory?{initialHistory:R.initialHistory}:{},...R.initialThinkingLevel?{initialThinkingLevel:R.initialThinkingLevel}:{},...R.onThinkingChange?{onThinkingChange:R.onThinkingChange}:{}}),Q=void 0!==R.banner&&0===(R.initialHistory?.length??0),[V,X]=n(!Q),Y=o(()=>{X(!0)},[]),Z=void 0===R.banner?void 0:k(R.banner,W.columns,{hints:INK_HINTS}),[tt,it]=n(0),ot=K.phase===l.idle&&K.draft.startsWith("/"),nt=ot&&!0===K.draft.split(/\s+/).at(-1)?.startsWith("/"),et=nt?u(K.draft,D):[],st=Math.max(et.length-1,0),rt=Math.min(tt,st);r((t,i)=>{K.phase===l.busy&&i.escape&&!i.meta?P():i.tab&&i.shift?G():i.meta&&"."===t?z(x(K.status.thinkingLevel,1)):i.meta&&","===t&&z(x(K.status.thinkingLevel,-1))});const mt=t=>{const i=t.trim();0!==i.length?(B(i),Y(),L(i),_(i)):q("")},at=K.phase===l.confirm&&void 0!==K.pendingConfirm?i(h,{prompt:K.pendingConfirm.prompt,args:K.pendingConfirm.args,width:W.columns,maxRows:W.promptRows+W.popupRows,onDecide:U}):i(p,{value:K.draft,width:W.columns,viewportRows:W.renderRows,maxRows:W.promptRows,showHint:W.showHelp,inputHistory:E,disabled:K.phase!==l.idle,...K.phase===l.cancelling?{disabledHint:"中断请求已发送,等待当前活动退出…"}:{},slashActive:ot,slashPopupActive:nt,autoApprove:K.status.autoApprove,onChange:q,onSubmit:mt,onSlashMove:t=>{it(i=>Math.min(Math.max(Math.min(i,st)+t,0),st))},onSlashComplete:()=>{const t=et[rt];if(t){const i=K.draft.split(/\s+/);i[i.length-1]=t.name,q(`${i.join(" ")} `),it(0)}},onSlashRun:i=>{const o=i.trim().split(/\s+/,1)[0]??"",n=g.some(t=>t.name===o),e=et[rt];if(!n&&"skill"===e?.kind){const t=K.draft.split(/\s+/);return t[t.length-1]=e.name,q(`${t.join(" ")} `),void it(0)}(i=>{Y(),q("");const o=i.trim();B(o);const n=o.split(/\s+/),e=n[0]??"",s=(n[1]??"").toLowerCase(),r=K.status.thinkingLevel;if("/compact"===e)return void J();if("/think"===e)return void z("off"===s?"off":"on"===s?"off"===r?"medium":r:"off"===r?"medium":"off");if("/effort"===e)return void("low"===s||"medium"===s||"high"===s?z(s):O({kind:"notice",id:t(),text:"用法: /effort low | medium | high"}));if("/auto"===e)return void("on"===s?F(!0):"off"===s?F(!1):G());if("/skills"===e){const[i,...o]=c(D,W.contentWidth);return void O({kind:"banner",id:t(),lines:[w(i??""),...o.map(t=>w(t,{dim:!0}))]})}if("/help"===e)return void O({kind:"notice",id:t(),text:H()});if("/exit"===e)return void $();const m=v(o,D);if(m)return 0===m.prompt.length?void q(`${m.skills.map(t=>`/${t.name}`).join(" ")} `):void mt(o);O({kind:"notice",id:t(),text:`未知命令 ${e}`})})(n?i:e?.name??i)}}),lt=y(K);return W.tooSmall?i(e,{width:W.columns,height:W.renderRows,alignItems:"center",justifyContent:"center",flexDirection:"column",overflow:"hidden"},i(s,{color:"yellow",bold:!0},"终端窗口过小"),i(s,{dimColor:!0},"请调整到至少 40 × 10;当前会话和草稿已保留")):i(e,{flexDirection:"column",width:W.columns,height:W.renderRows,overflow:"hidden"},i(S,{width:W.columns,history:K.history,live:K.live,onBannerSettled:Y,animateBanner:Q&&!V,navigationBlocked:K.phase===l.confirm||nt,...void 0===Z?{}:{banner:Z}}),i(e,{flexShrink:0,marginTop:void 0===lt?0:1},void 0===lt?i(d,{status:K.status,width:W.columns}):i(b,{activity:lt,width:W.columns})),ot?i(f,{matches:et,selected:rt,width:W.columns,maxRows:W.popupRows}):null,at)}
@@ -4,10 +4,9 @@ export declare function BannerLinesView({ lines, }: {
4
4
  readonly lines: readonly BannerLine[];
5
5
  }): ReactElement;
6
6
  /**
7
- * Animated banner entrance. Must be rendered OUTSIDE Ink's Static region (Static prints
8
- * each item exactly once, so interval-driven re-renders are never repainted there).
9
- * Calls `onSettled` once the animation completes (immediately when there is no logo),
10
- * letting the parent commit the final banner into Static.
7
+ * Animated banner entrance for the managed transcript viewport. Calls `onSettled` once the
8
+ * animation completes (immediately when there is no logo), so the parent can switch to the
9
+ * stable banner renderer without changing its semantic transcript entry.
11
10
  */
12
11
  export declare function BannerHistoryView({ lines, onSettled, }: {
13
12
  readonly lines: readonly BannerLine[];
@@ -2,6 +2,8 @@ import type { ReactElement } from "react";
2
2
  export interface ConfirmSelectProps {
3
3
  readonly prompt: string;
4
4
  readonly args: string;
5
+ readonly width: number;
6
+ readonly maxRows: number;
5
7
  readonly onDecide: (approved: boolean) => void;
6
8
  }
7
- export declare function ConfirmSelect({ prompt, args, onDecide }: ConfirmSelectProps): ReactElement;
9
+ export declare function ConfirmSelect({ prompt, args, width, maxRows, onDecide, }: ConfirmSelectProps): ReactElement;
@@ -1 +1 @@
1
- import{createElement as o,useState as r}from"react";import{Box as e,Text as n,useInput as t,useStdout as i}from"ink";export function ConfirmSelect({prompt:l,args:c,onDecide:s}){const[d,u]=r("no"),{stdout:m}=i(),f=m.columns||80;t((o,r)=>{if(r.leftArrow||r.rightArrow||r.upArrow||r.downArrow)return void u(o=>"yes"===o?"no":"yes");if(r.return||o.includes("\r")||o.includes("\n"))return void s("yes"===d);const e=o.toLowerCase();r.escape||"n"===e?s(!1):"y"===e&&s(!0)});const a=c.length>0&&"{}"!==c;return o(e,{flexDirection:"column",width:f},o(e,{flexDirection:"column",borderStyle:"round",borderColor:"yellow",paddingX:2},o(n,null,l),a?o(n,{dimColor:!0},c):null,o(e,{marginTop:1},o(n,"yes"===d?{color:"green"}:{},("yes"===d?"❯ ":" ")+"Yes"),o(n,"no"===d?{color:"green"}:{},` ${"no"===d?"❯ ":" "}No`))),o(e,{marginLeft:1},o(n,{dimColor:!0},"←→/y/n 选择 · Enter 确认 · Esc 取消 · Shift+Tab 自动批准本次及后续")))}
1
+ import{createElement as e,useState as r}from"react";import{Box as o,Text as n,useInput as t}from"ink";export function ConfirmSelect({prompt:i,args:l,width:d,maxRows:h,onDecide:c}){const[a,f]=r("no");t((e,r)=>{if(r.leftArrow||r.rightArrow||r.upArrow||r.downArrow)return void f(e=>"yes"===e?"no":"yes");if(r.return||e.includes("\r")||e.includes("\n"))return void c("yes"===a);const o=e.toLowerCase();r.escape||"n"===o?c(!1):"y"===o&&c(!0)});const s=l.length>0&&"{}"!==l,m=Math.max(1,Math.floor(h));return e(o,{flexDirection:"column",width:d,height:m,flexShrink:0,overflowY:"hidden"},e(o,{flexDirection:"column",borderStyle:"round",borderColor:"yellow",paddingX:2,height:Math.max(3,m-1),flexShrink:0,overflowY:"hidden"},e(n,null,i),s?e(n,{dimColor:!0},l):null,e(o,{marginTop:1},e(n,"yes"===a?{color:"green"}:{},("yes"===a?"❯ ":" ")+"Yes"),e(n,"no"===a?{color:"green"}:{},` ${"no"===a?"❯ ":" "}No`))),e(o,{marginLeft:1,height:1,flexShrink:0,overflowY:"hidden"},e(n,{dimColor:!0,wrap:"truncate-end"},"←→/y/n 选择 · Enter 确认 · Esc 取消 · Shift+Tab 自动批准本次及后续")))}
@@ -0,0 +1,13 @@
1
+ export interface ChatLayout {
2
+ readonly columns: number;
3
+ readonly rows: number;
4
+ readonly renderRows: number;
5
+ readonly contentWidth: number;
6
+ readonly promptRows: number;
7
+ readonly popupRows: number;
8
+ readonly showHelp: boolean;
9
+ readonly tooSmall: boolean;
10
+ }
11
+ export declare const MIN_CHAT_COLUMNS = 40;
12
+ export declare const MIN_CHAT_ROWS = 10;
13
+ export declare function resolveChatLayout(columns: number, rows: number): ChatLayout;
@@ -0,0 +1 @@
1
+ export const MIN_CHAT_COLUMNS=40;export const MIN_CHAT_ROWS=10;export function resolveChatLayout(o,t){const a=Math.max(1,Math.floor(o)),h=Math.max(1,Math.floor(t)),M=Math.max(1,h-1),r=M>=13,n=Math.min(12,Math.max(r?4:3,Math.floor(.4*M))),m=Math.max(3,Math.min(8,M-n-3));return{columns:a,rows:h,renderRows:M,contentWidth:Math.max(1,a-2),promptRows:n,popupRows:m,showHelp:r,tooSmall:a<40||h<10}}
@@ -3,6 +3,12 @@ export interface LineBufferState {
3
3
  readonly cursor: number;
4
4
  readonly goalColumn: number | undefined;
5
5
  }
6
+ export interface VisualLineMetrics {
7
+ readonly cursorRow: number;
8
+ readonly cursorColumn: number;
9
+ readonly totalRows: number;
10
+ }
11
+ export declare function visualLineMetrics(state: LineBufferState, columns: number): VisualLineMetrics;
6
12
  export declare function createLineBuffer(value: string, cursor?: number): LineBufferState;
7
13
  export declare function graphemeAt(value: string, offset: number): string;
8
14
  export declare function insertText(state: LineBufferState, text: string): LineBufferState;
@@ -1 +1 @@
1
- import{displayWidth as r}from"./display-width.js";const e=new Intl.Segmenter(void 0,{granularity:"grapheme"}),n=new Intl.Segmenter(void 0,{granularity:"word"});function t(r,e){return 0===e?0:r.lastIndexOf("\n",e-1)+1}function o(r,e){const n=t(r,e),o=r.indexOf("\n",e);return{start:n,end:-1===o?r.length:o}}function u(r){const[n]=e.segment(r);return n?.segment}function l(r,e){if(e>=r.length)return r.length;const n=u(r.slice(e));return void 0===n?r.length:e+n.length}function s(r,n){if(n<=0)return 0;const o=t(r,n);if(n===o)return o-1;let u=o;for(const t of e.segment(r.slice(o,n)))u=o+t.index;return u}function c(r,n,t){const o=Math.max(0,Math.min(n,r.length));if(0===o)return 0;if(o===r.length)return r.length;const u=e.segment(r).containing(o);return void 0===u||u.index===o?o:"backward"===t?u.index:u.index+u.segment.length}function a(r,e,n,t=""){const o=r.value.slice(0,e)+t+r.value.slice(n);return{value:o,cursor:c(o,e+t.length,"forward"),goalColumn:void 0}}function i(r){return!0===r.isWordLike}function f(r,e){let t=0;for(const o of n.segment(r.slice(0,e)))i(o)&&(t=o.index);return t}function v(r,e){for(const t of n.segment(r.slice(e)))if(i(t))return e+t.index+t.segment.length;return r.length}function g(e,n){return r(e.slice(t(e,n),n))}function d(n,t,o,u){let l=t,s=0;for(const c of e.segment(n.slice(t,o))){const e=r(c.segment);if(s+e>u)break;s+=e,l=t+c.index+c.segment.length}return l}function m(n,t,o){const u=Math.max(1,Math.floor(t)),l=[];let s=0;for(;s<=n.length;){const t=n.indexOf("\n",s),c=-1===t?n.length:t;let a=s,i=0;for(const t of e.segment(n.slice(s,c))){const e=s+t.index,n=r(t.segment);i>0&&i+n>u&&(l.push({start:a,end:e,softWrap:!0}),a=e,i=0),i+=n}const f=o===c&&i>=u;if(l.push({start:a,end:c,softWrap:f}),f&&l.push({start:c,end:c,softWrap:!1}),-1===t)break;s=t+1}return l}function h(r,e){for(const[n,t]of r.entries())if(e<t.end||e===t.end&&!t.softWrap)return n;return Math.max(0,r.length-1)}function x(e,n,t){const o=m(e.value,t,e.cursor),u=h(o,e.cursor),l=o[u],c=o[u+n];if(void 0===l||void 0===c)return e;const a=e.goalColumn??r(e.value.slice(l.start,e.cursor));let i=d(e.value,c.start,c.end,a);return c.softWrap&&i===c.end&&c.start<c.end&&(i=s(e.value,c.end)),{value:e.value,cursor:i,goalColumn:a}}function p(r){return void 0===r.goalColumn?r:{value:r.value,cursor:r.cursor,goalColumn:void 0}}export function createLineBuffer(r,e){return{value:r,cursor:c(r,Math.max(0,Math.min(e??r.length,r.length)),"backward"),goalColumn:void 0}}export function graphemeAt(r,e){return e<0||e>=r.length?"":u(r.slice(e))??""}export function insertText(r,e){return 0===e.length?p(r):a(r,r.cursor,r.cursor,e)}export function deleteBackward(r){if(0===r.cursor)return p(r);return a(r,s(r.value,r.cursor),r.cursor)}export function deleteForward(r){if(r.cursor>=r.value.length)return p(r);const e=l(r.value,r.cursor);return a(r,r.cursor,e)}export function deleteWordBackward(r){if(0===r.cursor)return p(r);return a(r,f(r.value,r.cursor),r.cursor)}export function killToLineStart(r){const{start:e}=o(r.value,r.cursor);return e===r.cursor?p(r):a(r,e,r.cursor)}export function killToLineEnd(r){const{end:e}=o(r.value,r.cursor);return r.cursor===e?e===r.value.length?p(r):a(r,r.cursor,e+1):a(r,r.cursor,e)}export function moveLeft(r){return 0===r.cursor?p(r):{value:r.value,cursor:s(r.value,r.cursor),goalColumn:void 0}}export function moveRight(r){return r.cursor>=r.value.length?p(r):{value:r.value,cursor:l(r.value,r.cursor),goalColumn:void 0}}export function moveWordLeft(r){return{value:r.value,cursor:f(r.value,r.cursor),goalColumn:void 0}}export function moveWordRight(r){return{value:r.value,cursor:v(r.value,r.cursor),goalColumn:void 0}}export function moveToLineStart(r){return{value:r.value,cursor:o(r.value,r.cursor).start,goalColumn:void 0}}export function moveToLineEnd(r){return{value:r.value,cursor:o(r.value,r.cursor).end,goalColumn:void 0}}export function moveUp(r){const{start:e}=o(r.value,r.cursor);if(0===e)return r;const n=r.goalColumn??g(r.value,r.cursor),t=o(r.value,e-1);return{value:r.value,cursor:d(r.value,t.start,t.end,n),goalColumn:n}}export function moveDown(r){const{end:e}=o(r.value,r.cursor);if(e===r.value.length)return r;const n=r.goalColumn??g(r.value,r.cursor),t=o(r.value,e+1);return{value:r.value,cursor:d(r.value,t.start,t.end,n),goalColumn:n}}export function moveVisualUp(r,e){return x(r,-1,e)}export function moveVisualDown(r,e){return x(r,1,e)}
1
+ import{displayWidth as r}from"./display-width.js";const e=new Intl.Segmenter(void 0,{granularity:"grapheme"}),n=new Intl.Segmenter(void 0,{granularity:"word"});function t(r,e){return 0===e?0:r.lastIndexOf("\n",e-1)+1}function o(r,e){const n=t(r,e),o=r.indexOf("\n",e);return{start:n,end:-1===o?r.length:o}}function u(r){const[n]=e.segment(r);return n?.segment}function l(r,e){if(e>=r.length)return r.length;const n=u(r.slice(e));return void 0===n?r.length:e+n.length}function s(r,n){if(n<=0)return 0;const o=t(r,n);if(n===o)return o-1;let u=o;for(const t of e.segment(r.slice(o,n)))u=o+t.index;return u}function c(r,n,t){const o=Math.max(0,Math.min(n,r.length));if(0===o)return 0;if(o===r.length)return r.length;const u=e.segment(r).containing(o);return void 0===u||u.index===o?o:"backward"===t?u.index:u.index+u.segment.length}function a(r,e,n,t=""){const o=r.value.slice(0,e)+t+r.value.slice(n);return{value:o,cursor:c(o,e+t.length,"forward"),goalColumn:void 0}}function i(r){return!0===r.isWordLike}function v(r,e){let t=0;for(const o of n.segment(r.slice(0,e)))i(o)&&(t=o.index);return t}function f(r,e){for(const t of n.segment(r.slice(e)))if(i(t))return e+t.index+t.segment.length;return r.length}function g(e,n){return r(e.slice(t(e,n),n))}function d(n,t,o,u){let l=t,s=0;for(const c of e.segment(n.slice(t,o))){const e=r(c.segment);if(s+e>u)break;s+=e,l=t+c.index+c.segment.length}return l}function m(n,t,o){const u=Math.max(1,Math.floor(t)),l=[];let s=0;for(;s<=n.length;){const t=n.indexOf("\n",s),c=-1===t?n.length:t;let a=s,i=0;for(const t of e.segment(n.slice(s,c))){const e=s+t.index,n=r(t.segment);i>0&&i+n>u&&(l.push({start:a,end:e,softWrap:!0}),a=e,i=0),i+=n}const v=o===c&&i>=u,f=-1===t&&v;if(l.push({start:a,end:c,softWrap:v}),f&&l.push({start:c,end:c,softWrap:!1}),-1===t)break;s=t+1}return l}function h(r,e){for(const[n,t]of r.entries())if(e<t.end||e===t.end&&!t.softWrap)return n;return Math.max(0,r.length-1)}export function visualLineMetrics(e,n){const t=m(e.value,n,e.cursor),o=h(t,e.cursor),u=t[o];return{cursorRow:o,cursorColumn:void 0===u?0:r(e.value.slice(u.start,e.cursor)),totalRows:Math.max(1,t.length)}}function x(e,n,t){const o=m(e.value,t,e.cursor),u=h(o,e.cursor),l=o[u],c=o[u+n];if(void 0===l||void 0===c)return e;const a=e.goalColumn??r(e.value.slice(l.start,e.cursor));let i=d(e.value,c.start,c.end,a);return c.softWrap&&i===c.end&&c.start<c.end&&(i=s(e.value,c.end)),{value:e.value,cursor:i,goalColumn:a}}function p(r){return void 0===r.goalColumn?r:{value:r.value,cursor:r.cursor,goalColumn:void 0}}export function createLineBuffer(r,e){return{value:r,cursor:c(r,Math.max(0,Math.min(e??r.length,r.length)),"backward"),goalColumn:void 0}}export function graphemeAt(r,e){return e<0||e>=r.length?"":u(r.slice(e))??""}export function insertText(r,e){return 0===e.length?p(r):a(r,r.cursor,r.cursor,e)}export function deleteBackward(r){if(0===r.cursor)return p(r);return a(r,s(r.value,r.cursor),r.cursor)}export function deleteForward(r){if(r.cursor>=r.value.length)return p(r);const e=l(r.value,r.cursor);return a(r,r.cursor,e)}export function deleteWordBackward(r){if(0===r.cursor)return p(r);return a(r,v(r.value,r.cursor),r.cursor)}export function killToLineStart(r){const{start:e}=o(r.value,r.cursor);return e===r.cursor?p(r):a(r,e,r.cursor)}export function killToLineEnd(r){const{end:e}=o(r.value,r.cursor);return r.cursor===e?e===r.value.length?p(r):a(r,r.cursor,e+1):a(r,r.cursor,e)}export function moveLeft(r){return 0===r.cursor?p(r):{value:r.value,cursor:s(r.value,r.cursor),goalColumn:void 0}}export function moveRight(r){return r.cursor>=r.value.length?p(r):{value:r.value,cursor:l(r.value,r.cursor),goalColumn:void 0}}export function moveWordLeft(r){return{value:r.value,cursor:v(r.value,r.cursor),goalColumn:void 0}}export function moveWordRight(r){return{value:r.value,cursor:f(r.value,r.cursor),goalColumn:void 0}}export function moveToLineStart(r){return{value:r.value,cursor:o(r.value,r.cursor).start,goalColumn:void 0}}export function moveToLineEnd(r){return{value:r.value,cursor:o(r.value,r.cursor).end,goalColumn:void 0}}export function moveUp(r){const{start:e}=o(r.value,r.cursor);if(0===e)return r;const n=r.goalColumn??g(r.value,r.cursor),t=o(r.value,e-1);return{value:r.value,cursor:d(r.value,t.start,t.end,n),goalColumn:n}}export function moveDown(r){const{end:e}=o(r.value,r.cursor);if(e===r.value.length)return r;const n=r.goalColumn??g(r.value,r.cursor),t=o(r.value,e+1);return{value:r.value,cursor:d(r.value,t.start,t.end,n),goalColumn:n}}export function moveVisualUp(r,e){return x(r,-1,e)}export function moveVisualDown(r,e){return x(r,1,e)}
@@ -0,0 +1,9 @@
1
+ export declare const ENABLE_MOUSE_TRACKING = "\u001B[?1000h\u001B[?1006h";
2
+ export declare const DISABLE_MOUSE_TRACKING = "\u001B[?1006l\u001B[?1000l";
3
+ export interface MouseWheelInput {
4
+ readonly direction: "up" | "down";
5
+ readonly column: number;
6
+ readonly row: number;
7
+ }
8
+ export declare function isMouseProtocolInput(input: string): boolean;
9
+ export declare function parseMouseWheelInput(input: string): MouseWheelInput | undefined;
@@ -0,0 +1 @@
1
+ export const ENABLE_MOUSE_TRACKING="[?1000h[?1006h";export const DISABLE_MOUSE_TRACKING="[?1006l[?1000l";const t=["[<","›<","[<"];function o(o){const n=t.find(t=>o.startsWith(t)),e=o.at(-1);if(void 0===n||"M"!==e&&"m"!==e)return;const r=o.slice(n.length,-1).split(";");if(3!==r.length||r.some(t=>!/^\d+$/.test(t)))return;const[u,i,s]=r,c=Number(u),d=Number(i),l=Number(s);return Number.isInteger(c)&&Number.isInteger(d)&&Number.isInteger(l)?{button:c,column:d,row:l}:void 0}export function isMouseProtocolInput(t){return void 0!==o(t)}export function parseMouseWheelInput(t){const n=o(t),e=void 0===n?void 0:3&n.button;if(void 0!==n&&64&n.button&&void 0!==e&&!(e>1))return{direction:0===e?"up":"down",column:n.column,row:n.row}}
@@ -1,7 +1,6 @@
1
1
  import type { AgentSession } from "@roll-agent/runtime";
2
2
  import type { ThinkingLevel } from "../../../llm/providers.ts";
3
- import { type BannerInfo } from "../banner.ts";
4
- export declare const INK_HINTS = "/exit \u9000\u51FA \u00B7 Esc \u4E2D\u65AD \u00B7 / \u547D\u4EE4 \u00B7 Shift+Enter/Ctrl+J \u6362\u884C \u00B7 Alt+./Alt+, \u8C03\u63A8\u7406 \u00B7 Shift+Tab \u81EA\u52A8\u6279\u51C6";
3
+ import type { BannerInfo } from "../banner.ts";
5
4
  export interface InkReplStore {
6
5
  updateTitle(threadId: string, title: string): void;
7
6
  countMessages(threadId: string): number;
@@ -12,5 +11,6 @@ export interface RunInkReplOptions {
12
11
  readonly banner?: BannerInfo;
13
12
  readonly initialThinkingLevel?: ThinkingLevel;
14
13
  readonly onThinkingChange?: (level: ThinkingLevel) => void;
14
+ readonly onStarted?: () => void;
15
15
  }
16
16
  export declare function runInkRepl(session: AgentSession, store: InkReplStore, isNewSession: boolean, options: RunInkReplOptions): Promise<void>;
@@ -1 +1 @@
1
- import{createElement as t}from"react";import{render as e}from"ink";import{ChatApp as i}from"./app.js";import{messagesToHistory as n}from"./history-from-messages.js";import{titleFromMessage as o}from"../title.js";import{buildBannerLines as s}from"../banner.js";import{log as a}from"../../utils/output.js";export const INK_HINTS="/exit 退出 · Esc 中断 · / 命令 · Shift+Enter/Ctrl+J 换行 · Alt+./Alt+, 调推理 · Shift+Tab 自动批准";export async function runInkRepl(r,l,d,m){let p=!1,c=!d;const g=t=>{p=!0,c||(l.updateTitle(r.id,o(t)),c=!0)};m.banner||a.info(`多轮对话已就绪(${INK_HINTS})`);const h=n(r.getMessages()),u=m.banner?s(m.banner,process.stdout.columns||80,{hints:INK_HINTS}):void 0,f=void 0!==u&&0===h.length,T=void 0===u||f?[]:[{kind:"banner",id:"banner",lines:u}],k=!0===process.stdin.isTTY&&"function"==typeof process.stdin.setRawMode;let b;k&&process.stdin.setRawMode(!0);try{b=e(t(i,{session:r,model:m.model,contextWindow:r.getContextWindow(),availableSkills:r.getSkillSummaries(),onUserSubmit:g,onExit:()=>{b.unmount()},initialHistory:[...T,...h],...f&&void 0!==u?{animatedBanner:u}:{},...m.initialThinkingLevel?{initialThinkingLevel:m.initialThinkingLevel}:{},...m.onThinkingChange?{onThinkingChange:m.onThinkingChange}:{}}),{kittyKeyboard:{mode:"auto",flags:["disambiguateEscapeCodes","reportAlternateKeys"]}})}catch(t){throw k&&process.stdin.setRawMode(!1),t}await b.waitUntilExit(),await r.close(),d&&!p&&0===l.countMessages(r.id)&&l.deleteThread(r.id)}
1
+ import{createElement as t}from"react";import{render as e}from"ink";import{ChatApp as i,INK_HINTS as n}from"./app.js";import{messagesToHistory as o}from"./history-from-messages.js";import{titleFromMessage as s}from"../title.js";import{log as r}from"../../utils/output.js";import{createChatTerminalOutput as a}from"./terminal-output.js";export async function runInkRepl(l,m,d,g){let p=!1,u=!d;const c=t=>{p=!0,u||(m.updateTitle(l.id,s(t)),u=!0)};g.banner||r.info(`多轮对话已就绪(${n})`);const f=o(l.getMessages()),h=a(process.stdout);try{const n=e(t(i,{session:l,model:g.model,contextWindow:l.getContextWindow(),availableSkills:l.getSkillSummaries(),onUserSubmit:c,onExit:()=>{n.unmount()},initialHistory:f,...void 0===g.banner?{}:{banner:g.banner},...g.initialThinkingLevel?{initialThinkingLevel:g.initialThinkingLevel}:{},...g.onThinkingChange?{onThinkingChange:g.onThinkingChange}:{}}),{stdout:h.stdout,interactive:!0,alternateScreen:!0,kittyKeyboard:{mode:"auto",flags:["disambiguateEscapeCodes","reportAlternateKeys"]}});g.onStarted?.(),await n.waitUntilExit()}finally{h.dispose()}if(await l.close(),d&&!p&&0===m.countMessages(l.id))return m.deleteThread(l.id),void process.stderr.write("本次会话无消息,未保存\n");const k=l.getMessages().filter(t=>"user"===t.role||"assistant"===t.role).length;process.stderr.write(`会话 ${l.id} · ${String(k)} 条消息\n继续:roll chat --session ${l.id}\n`)}
@@ -3,5 +3,7 @@ import type { SlashEntry } from "./commands.ts";
3
3
  export interface SlashPopupProps {
4
4
  readonly matches: readonly SlashEntry[];
5
5
  readonly selected: number;
6
+ readonly width: number;
7
+ readonly maxRows: number;
6
8
  }
7
- export declare function SlashPopup({ matches, selected }: SlashPopupProps): ReactElement;
9
+ export declare function SlashPopup({ matches, selected, width, maxRows }: SlashPopupProps): ReactElement;
@@ -1 +1 @@
1
- import{createElement as o}from"react";import{Box as r,Text as t,useStdout as n}from"ink";const e=8;export function SlashPopup({matches:e,selected:l}){const{stdout:a}=n(),i=a.columns||80;if(0===e.length)return o(r,{borderStyle:"round",borderColor:"gray",paddingX:1},o(t,{dimColor:!0},"无匹配命令"));const d=Math.min(Math.max(l-8+1,0),Math.max(e.length-8,0)),m=e.slice(d,d+8),c=Math.max(i-4,20);return o(r,{flexDirection:"column",borderStyle:"round",borderColor:"gray",paddingX:1},...m.map((n,e)=>{const a=d+e===l;return o(r,{key:n.name,width:c},o(t,{wrap:"truncate-end"},o(t,a?{color:"cyan",bold:!0}:{dimColor:!0},`${a?"❯ ":" "}${n.name}`),"skill"===n.kind?o(t,{color:"magenta"}," ⚡"):null,o(t,{dimColor:!0},` ${n.description}`)))}),e.length>8?o(t,{dimColor:!0},` ${String(l+1)}/${String(e.length)} · ↑↓ 浏览`):null)}
1
+ import{createElement as r}from"react";import{Box as e,Text as o}from"ink";const t=8;export function SlashPopup({matches:t,selected:n,width:a,maxRows:i}){if(0===t.length)return r(e,{borderStyle:"round",borderColor:"gray",paddingX:1,height:Math.min(3,i),flexShrink:0,overflowY:"hidden"},r(o,{dimColor:!0},"无匹配命令"));const l=Math.max(1,i-2),d=l>=2&&t.length>l,h=Math.max(1,Math.min(8,l-(d?1:0))),m=Math.min(Math.max(n-h+1,0),Math.max(t.length-h,0)),c=t.slice(m,m+h),g=Math.min(i,h+2+(d?1:0)),s=Math.max(a-4,20);return r(e,{flexDirection:"column",borderStyle:"round",borderColor:"gray",paddingX:1,height:g,flexShrink:0,overflowY:"hidden"},...c.map((t,a)=>{const i=m+a===n;return r(e,{key:t.name,width:s},r(o,{wrap:"truncate-end"},r(o,i?{color:"cyan",bold:!0}:{dimColor:!0},`${i?"❯ ":" "}${t.name}`),"skill"===t.kind?r(o,{color:"magenta"}," ⚡"):null,r(o,{dimColor:!0},` ${t.description}`)))}),d?r(o,{dimColor:!0},` ${String(n+1)}/${String(t.length)} · ↑↓ 浏览`):null)}
@@ -10,7 +10,8 @@ export interface StatusSegmentView {
10
10
  readonly props: SegmentProps;
11
11
  }
12
12
  export declare function composeStatusSegments(status: StatusState, width: number): StatusSegmentView[];
13
- export declare function StatusLine({ status }: {
14
- status: StatusState;
13
+ export declare function StatusLine({ status, width, }: {
14
+ readonly status: StatusState;
15
+ readonly width: number;
15
16
  }): ReactElement;
16
17
  export {};
@@ -1 +1 @@
1
- import{createElement as o}from"react";import{Box as t,Text as e,useStdout as s}from"ink";import{computeUsageParts as n,contextPressure as p,formatContextUsage as i,formatThroughput as r,formatTokens as l,formatTurnUsage as c}from"../../utils/token-format.js";import{displayWidth as u}from"./display-width.js";import{GLYPHS as m}from"../../utils/glyphs.js";import{thinkingLabel as a}from"./thinking.js";const k={ok:{dimColor:!0},warn:{color:"yellow"},critical:{color:"red"}},d={off:"off",low:"low",medium:"med",high:"high"},f=["tps","session","turn","think"],h=" · ";export function composeStatusSegments(o,t){const e=n(o.turnUsage,o.sessionUsage,o.contextWindow,o.contextInputTokens),s=[{key:"model",full:o.model,compact:o.model,props:{color:"magenta"}},{key:"think",full:a(o.thinkingLevel),compact:`${m.think} ${d[o.thinkingLevel]}`,props:"off"===o.thinkingLevel?{color:"yellow"}:{dimColor:!0}}];o.autoApprove&&s.push({key:"auto",full:`${m.auto} auto-approve`,compact:`${m.auto} auto`,props:{color:"yellow"}});const y=i(e);void 0!==y&&void 0!==e.usedTokens&&void 0!==e.contextWindow&&s.push({key:"ctx",full:y,compact:`ctx ${l(e.usedTokens)}/${l(e.contextWindow)}`,props:k[p(e.percentLeft)]});const v=c(e);if(void 0!==v){const o=[];void 0!==e.inputTokens&&o.push(`↑${l(e.inputTokens)}`),void 0!==e.outputTokens&&o.push(`↓${l(e.outputTokens)}`),s.push({key:"turn",full:v,compact:o.join(" "),props:{dimColor:!0}})}const g=r(o.outputTokensPerSecond);void 0!==g&&s.push({key:"tps",full:g,compact:g.replace(" tok/s","t/s"),props:{dimColor:!0}}),void 0!==e.sessionTokens&&s.push({key:"session",full:`session ${l(e.sessionTokens)}`,compact:`Σ${l(e.sessionTokens)}`,props:{dimColor:!0}});const w=(o,t)=>o.map(o=>({key:o.key,text:o[t],props:o.props})),x=o=>u(o.map(o=>o.text).join(h))<=t,T=w(s,"full");if(x(T))return T;let $=s,j=w($,"compact");for(const o of f){if(x(j))break;$=$.filter(t=>t.key!==o),j=w($,"compact")}return j}export function StatusLine({status:n}){const{stdout:p}=s(),i=p.columns||80,r=composeStatusSegments(n,i);return o(t,{width:i},o(e,{wrap:"truncate-end"},...r.flatMap((t,s)=>[...s>0?[o(e,{key:`${t.key}-sep`,dimColor:!0},h)]:[],o(e,{key:t.key,...t.props},t.text)])))}
1
+ import{createElement as o}from"react";import{Box as t,Text as e}from"ink";import{computeUsageParts as s,contextPressure as n,formatContextUsage as p,formatThroughput as i,formatTokens as r,formatTurnUsage as l}from"../../utils/token-format.js";import{displayWidth as c}from"./display-width.js";import{GLYPHS as u}from"../../utils/glyphs.js";import{thinkingLabel as m}from"./thinking.js";const a={ok:{dimColor:!0},warn:{color:"yellow"},critical:{color:"red"}},k={off:"off",low:"low",medium:"med",high:"high"},d=["tps","session","turn","think"],f=" · ";export function composeStatusSegments(o,t){const e=s(o.turnUsage,o.sessionUsage,o.contextWindow,o.contextInputTokens),h=[{key:"model",full:o.model,compact:o.model,props:{color:"magenta"}},{key:"think",full:m(o.thinkingLevel),compact:`${u.think} ${k[o.thinkingLevel]}`,props:"off"===o.thinkingLevel?{color:"yellow"}:{dimColor:!0}}];o.autoApprove&&h.push({key:"auto",full:`${u.auto} auto-approve`,compact:`${u.auto} auto`,props:{color:"yellow"}});const y=p(e);void 0!==y&&void 0!==e.usedTokens&&void 0!==e.contextWindow&&h.push({key:"ctx",full:y,compact:`ctx ${r(e.usedTokens)}/${r(e.contextWindow)}`,props:a[n(e.percentLeft)]});const v=l(e);if(void 0!==v){const o=[];void 0!==e.inputTokens&&o.push(`↑${r(e.inputTokens)}`),void 0!==e.outputTokens&&o.push(`↓${r(e.outputTokens)}`),h.push({key:"turn",full:v,compact:o.join(" "),props:{dimColor:!0}})}const w=i(o.outputTokensPerSecond);void 0!==w&&h.push({key:"tps",full:w,compact:w.replace(" tok/s","t/s"),props:{dimColor:!0}}),void 0!==e.sessionTokens&&h.push({key:"session",full:`session ${r(e.sessionTokens)}`,compact:`Σ${r(e.sessionTokens)}`,props:{dimColor:!0}});const g=(o,t)=>o.map(o=>({key:o.key,text:o[t],props:o.props})),x=o=>c(o.map(o=>o.text).join(f))<=t,T=g(h,"full");if(x(T))return T;let $=h,j=g($,"compact");for(const o of d){if(x(j))break;$=$.filter(t=>t.key!==o),j=g($,"compact")}return j}export function StatusLine({status:s,width:n}){const p=composeStatusSegments(s,n);return o(t,{width:n},o(e,{wrap:"truncate-end"},...p.flatMap((t,s)=>[...s>0?[o(e,{key:`${t.key}-sep`,dimColor:!0},f)]:[],o(e,{key:t.key,...t.props},t.text)])))}
@@ -0,0 +1,6 @@
1
+ export declare const CHAT_CURSOR_REFRESH_EVENT = "roll:chat-cursor-refresh";
2
+ export interface ChatTerminalOutput {
3
+ readonly stdout: NodeJS.WriteStream;
4
+ dispose(): void;
5
+ }
6
+ export declare function createChatTerminalOutput(source: NodeJS.WriteStream): ChatTerminalOutput;
@@ -0,0 +1 @@
1
+ import{Writable as e}from"node:stream";const s="",r="",i="[?2026h",t="[?2026l",n="[?25l",o="[?25h",u=16,h=["[?1049l","[?25h","[?2004l","[?1000l","[?1006l","[<u"],c=String.fromCharCode(27),a="\\[[0-?]*[ -/]*[@-~]",l=new RegExp(`${c}${a}`,"g"),d=new RegExp(`(?:${c}${a})+$`,"g");export const CHAT_CURSOR_REFRESH_EVENT="roll:chat-cursor-refresh";function p(e){let s=e;for(const e of h)s=s.replaceAll(e,"");return 0===s.length}function z(e){return e.replace(l,"").replace(/[\r\n\t]/g,"").length>0}function m(e){return/[\r\n]$/.test(e.replace(d,""))}class ManagedChatOutput extends e{source;renderedColumns;nextColumnsRead;suppressNextResizeClear=!1;suppressVisualWrites=!1;resizeSynchronizationOpen=!1;resizeFrameNeedsCursorLineAdvance=!1;resizeTimer;cursorRefreshQueued=!1;handleSourceResize=()=>{void 0!==this.resizeTimer&&clearTimeout(this.resizeTimer),this.resizeTimer=setTimeout(()=>{this.resizeTimer=void 0;const e=this.source.columns,s=this.listeners("resize"),t=s[0],n=Math.max(1,Math.min(this.renderedColumns,e)-1);if(void 0!==t&&n<this.renderedColumns){this.suppressVisualWrites=!0;try{for(const e of s.slice(1))e.call(this)}finally{this.suppressVisualWrites=!1}this.source.write(i+r),this.resizeSynchronizationOpen=!0,this.nextColumnsRead=n,this.suppressNextResizeClear=!0;try{t()}finally{this.nextColumnsRead=void 0,this.suppressNextResizeClear=!1}this.suppressVisualWrites=!0;try{t()}finally{this.suppressVisualWrites=!1}}else this.emit("resize");this.renderedColumns=e},u)};constructor(e){super({decodeStrings:!1}),this.source=e,this.renderedColumns=e.columns,e.on("resize",this.handleSourceResize)}get columns(){if(void 0!==this.nextColumnsRead){const e=this.nextColumnsRead;return this.nextColumnsRead=void 0,e}return this.source.columns}get rows(){return this.source.rows}get isTTY(){return!0}_write(e,r,u){const h=(Buffer.isBuffer(e)?e.toString("utf8"):e).replaceAll(s,"");if(this.suppressNextResizeClear)return this.suppressNextResizeClear=!1,void u();if(void 0===this.resizeTimer&&!this.suppressVisualWrites||p(h))try{h.includes(i)&&(this.resizeSynchronizationOpen=!0,this.resizeFrameNeedsCursorLineAdvance=!1);const e=this.resizeSynchronizationOpen&&this.resizeFrameNeedsCursorLineAdvance&&h.includes(o)&&!z(h)?"\r\n":"";this.source.write(e+h),e.length>0&&(this.resizeFrameNeedsCursorLineAdvance=!1),this.resizeSynchronizationOpen&&z(h)&&(this.resizeFrameNeedsCursorLineAdvance=!m(h)),h.includes(t)&&(this.resizeSynchronizationOpen=!1,this.resizeFrameNeedsCursorLineAdvance=!1),h.includes(n)&&!h.includes(o)&&this.queueCursorRefresh(),u()}catch(e){u(e instanceof Error?e:new Error(String(e)))}else u()}queueCursorRefresh(){this.cursorRefreshQueued||(this.cursorRefreshQueued=!0,queueMicrotask(()=>{this.cursorRefreshQueued=!1,this.destroyed||this.emit(CHAT_CURSOR_REFRESH_EVENT)}))}dispose(){void 0!==this.resizeTimer&&(clearTimeout(this.resizeTimer),this.resizeTimer=void 0),this.source.off("resize",this.handleSourceResize),this.resizeSynchronizationOpen&&(this.source.write(t),this.resizeSynchronizationOpen=!1,this.resizeFrameNeedsCursorLineAdvance=!1),this.destroy()}}export function createChatTerminalOutput(e){const s=new ManagedChatOutput(e);return{stdout:s,dispose:()=>s.dispose()}}
@@ -1,6 +1,10 @@
1
1
  import type { ReactElement } from "react";
2
2
  export interface TextPromptProps {
3
3
  readonly value: string;
4
+ readonly width: number;
5
+ readonly viewportRows: number;
6
+ readonly maxRows: number;
7
+ readonly showHint: boolean;
4
8
  readonly inputHistory: readonly string[];
5
9
  readonly disabled: boolean;
6
10
  readonly disabledHint?: string;
@@ -1 +1 @@
1
- import{createElement as r,useLayoutEffect as t,useRef as e,useState as n}from"react";import{Box as o,Text as i,useInput as u,usePaste as c,useStdout as l}from"ink";import{GLYPHS as s}from"../../utils/glyphs.js";import{applyEditorCommand as a,resolveEditorCommand as d}from"./editor-keymap.js";import{createLineBuffer as f,graphemeAt as v,insertText as h,moveVisualDown as p,moveVisualUp as m}from"./line-buffer.js";const w=2,g=4,A=2;function S(r){return/^\[\?\d+u$/.test(r)||/^\x9b\?\d+u$/.test(r)}export function cursorPositionOf(r,t){let e=t;for(const[t,n]of r.entries()){if(e<=n.length)return{row:t,col:e};e-=n.length+1}const n=r.length-1;return{row:n,col:r[n]?.length??0}}export function TextPrompt(w){const{value:g,inputHistory:A,disabled:b,slashActive:y,slashPopupActive:C,autoApprove:x,onChange:E,onSubmit:k}=w,{stdout:j}=l(),P=j.columns||80,T=Math.max(1,P-2-4-2),[$]=n(()=>f(g)),H=e($),M=e(void 0),D=e(""),[,J]=n(0);t(()=>{H.current.value!==g&&(H.current=f(g),M.current=void 0,D.current="",J(r=>r+1))},[g]);const O=r=>{const t=H.current.value!==r.value;H.current=r,J(r=>r+1),t&&E(r.value)},L=()=>{M.current=void 0,D.current=""};u((r,t)=>{if(t.meta&&("."===r||","===r))return;if(S(r))return;if("\n"===r||t.ctrl&&"j"===r||t.return&&(t.shift||t.meta))return L(),void O(h(H.current,"\n"));if(t.return||r.includes("\r")){L();const t=r.split("\r",1)[0]??"",e=t.length>0?h(H.current,t):H.current;return y||e.value.startsWith("/")?void w.onSlashRun(e.value):void k(e.value)}if(t.upArrow||t.downArrow){const r=void 0!==M.current,e=t.upArrow&&0===H.current.value.length;if((r||e)&&(r=>{const t=M.current;if(void 0===t){if(1===r||H.current.value.length>0||0===A.length)return!1;const t=A.length-1,e=A[t];return void 0!==e&&(D.current=H.current.value,M.current=t,O(f(e)),!0)}const e=t+r;if(e<0)return!0;if(e>=A.length){const r=D.current;return L(),O(f(r)),!0}const n=A[e];return void 0===n?(L(),!1):(M.current=e,O(f(n)),!0)})(t.upArrow?-1:1))return}if(C){if(t.upArrow)return void w.onSlashMove(-1);if(t.downArrow)return void w.onSlashMove(1);if(t.tab&&!t.shift)return void w.onSlashComplete()}if(y&&t.escape)return L(),void O(f(""));const e=d(r,t);if(void 0!==e){L();const r="move-up"===e?m(H.current,T):"move-down"===e?p(H.current,T):a(e,H.current);return void O(r)}t.ctrl||t.escape||t.tab||t.upArrow||t.downArrow||t.leftArrow||t.rightArrow||r.length>0&&(L(),O(h(H.current,r)))},{isActive:!b}),c(r=>{L(),O(h(H.current,r.replace(/\r\n?/g,"\n")))},{isActive:!b});const R=H.current,W=R.value.split("\n"),X=cursorPositionOf(W,R.cursor),q=r(o,{flexDirection:"column"},...W.map((t,e)=>{const n=r(o,{width:2,flexShrink:0},0===e?r(i,b?{dimColor:!0}:{color:"green"},"› "):r(i,null," "));if(b)return r(o,{key:String(e)},n,r(i,{dimColor:!0,wrap:"hard"},t));if(e!==X.row)return r(o,{key:String(e)},n,r(i,{wrap:"hard"},t));const u=v(t,X.col),c=t.slice(0,X.col),l=t.slice(X.col+u.length);return r(o,{key:String(e)},n,r(i,{wrap:"hard"},c,r(i,{inverse:!0},""===u?" ":u),l))})),z=b?w.disabledHint??"Esc 中断本轮":y?"↑↓ 选择 · Tab 补全 · Enter 执行 · Esc 取消":x?"Shift+Tab 关闭 · Enter 发送 · 空输入 ↑ 历史 · Shift+Enter/Ctrl+J 换行 · / 命令":"Enter 发送 · 空输入 ↑ 历史 · Shift+Enter/Ctrl+J 换行 · / 命令 · Shift+Tab 自动批准",B=b&&void 0!==w.disabledHint?{color:"yellow",dimColor:!0}:{dimColor:!0},F=r(o,{marginLeft:1},...x?[r(i,{color:"yellow"},`${s.auto} auto`),r(i,B,` · ${z}`)]:[r(i,B,z)]);return r(o,{flexDirection:"column",width:P},r(o,{borderStyle:"round",borderColor:b?"gray":"cyan",paddingX:2},q),F)}
1
+ import{createElement as r,useEffect as t,useLayoutEffect as e,useRef as o,useState as n}from"react";import{Box as i,Text as u,useCursor as l,useInput as c,usePaste as a,useStdout as s}from"ink";import{GLYPHS as d}from"../../utils/glyphs.js";import{applyEditorCommand as h,resolveEditorCommand as f}from"./editor-keymap.js";import{createLineBuffer as v,insertText as m,moveVisualDown as p,moveVisualUp as w,visualLineMetrics as g}from"./line-buffer.js";import{isMouseProtocolInput as x}from"./mouse-input.js";import{CHAT_CURSOR_REFRESH_EVENT as A}from"./terminal-output.js";const S=2,b=4,C=2,y=5;function M(r){return/^\[\?\d+u$/.test(r)||/^\x9b\?\d+u$/.test(r)}export function cursorPositionOf(r,t){let e=t;for(const[t,o]of r.entries()){if(e<=o.length)return{row:t,col:e};e-=o.length+1}const o=r.length-1;return{row:o,col:r[o]?.length??0}}export function TextPrompt(S){const{value:b,width:C,maxRows:y,showHint:E,inputHistory:R,disabled:k,slashActive:j,slashPopupActive:H,autoApprove:P,onChange:T,onSubmit:$}=S,{setCursorPosition:D}=l(),{stdout:Y}=s(),[,J]=n(0);t(()=>{if(k)return;const r=()=>{J(r=>r+1)};return Y.on(A,r),()=>{Y.off(A,r)}},[k,Y]);const L=Math.max(1,C-2-4-2),[O]=n(()=>v(b)),W=o(O),X=o(void 0),q=o(""),[,z]=n(0);e(()=>{W.current.value!==b&&(W.current=v(b),X.current=void 0,q.current="",z(r=>r+1))},[b]);const B=r=>{const t=W.current.value!==r.value;W.current=r,z(r=>r+1),t&&T(r.value)},F=()=>{X.current=void 0,q.current=""};c((r,t)=>{if(t.meta&&("."===r||","===r))return;if(M(r))return;if(x(r))return;if("\n"===r||t.ctrl&&"j"===r||t.return&&(t.shift||t.meta))return F(),void B(m(W.current,"\n"));if(t.return||r.includes("\r")){F();const t=r.split("\r",1)[0]??"",e=t.length>0?m(W.current,t):W.current;return j||e.value.startsWith("/")?void S.onSlashRun(e.value):void $(e.value)}if(t.upArrow||t.downArrow){const r=void 0!==X.current,e=t.upArrow&&0===W.current.value.length;if((r||e)&&(r=>{const t=X.current;if(void 0===t){if(1===r||W.current.value.length>0||0===R.length)return!1;const t=R.length-1,e=R[t];return void 0!==e&&(q.current=W.current.value,X.current=t,B(v(e)),!0)}const e=t+r;if(e<0)return!0;if(e>=R.length){const r=q.current;return F(),B(v(r)),!0}const o=R[e];return void 0===o?(F(),!1):(X.current=e,B(v(o)),!0)})(t.upArrow?-1:1))return}if(H){if(t.upArrow)return void S.onSlashMove(-1);if(t.downArrow)return void S.onSlashMove(1);if(t.tab&&!t.shift)return void S.onSlashComplete()}if(j&&t.escape)return F(),void B(v(""));const e=f(r,t);if(void 0!==e){F();const r="move-up"===e?w(W.current,L):"move-down"===e?p(W.current,L):h(e,W.current);return void B(r)}t.ctrl||t.escape||t.tab||t.upArrow||t.downArrow||t.leftArrow||t.rightArrow||r.length>0&&(F(),B(m(W.current,r)))},{isActive:!k}),a(r=>{F(),B(m(W.current,r.replace(/\r\n?/g,"\n")))},{isActive:!k});const G=W.current,I=G.value.split("\n"),K=g(G,L),N=Math.max(1,Math.floor(y)-2-(E?1:0)),Q=Math.min(K.totalRows,N),U=Math.min(Math.max(0,K.cursorRow-Q+1),Math.max(0,K.totalRows-Q)),V=K.cursorRow-U,Z=Q+2+(E?1:0),_=Math.max(0,S.viewportRows-Z);D(k?void 0:{x:5+K.cursorColumn,y:_+1+V});const rr=r(i,{flexDirection:"column",flexShrink:0,position:"relative",top:-U,height:K.totalRows},...I.map((t,e)=>{const o=r(i,{width:2,flexShrink:0},0===e?r(u,k?{dimColor:!0}:{color:"green"},"› "):r(u,null," "));return r(i,{key:String(e)},o,r(u,k?{dimColor:!0,wrap:"hard"}:{wrap:"hard"},t))})),tr=r(i,{flexDirection:"column",height:Q,overflowY:"hidden"},rr),er=k?S.disabledHint??"Esc 中断本轮":j?"↑↓ 选择 · Tab 补全 · Enter 执行 · Esc 取消":P?"Shift+Tab 关闭 · Enter 发送 · 空输入 ↑ 历史 · Shift+Enter/Ctrl+J 换行 · / 命令":"Enter 发送 · 空输入 ↑ 历史 · Shift+Enter/Ctrl+J 换行 · / 命令 · Shift+Tab 自动批准",or=k&&void 0!==S.disabledHint?{color:"yellow",dimColor:!0}:{dimColor:!0},nr=E?r(i,{marginLeft:1,flexShrink:0,height:1,overflowY:"hidden"},...P?[r(u,{color:"yellow",wrap:"truncate-end"},`${d.auto} auto`),r(u,{...or,wrap:"truncate-end"},` · ${er}`)]:[r(u,{...or,wrap:"truncate-end"},er)]):null;return r(i,{flexDirection:"column",width:C,flexShrink:0},r(i,{borderStyle:"round",borderColor:k?"gray":"cyan",paddingX:2,height:Q+2,overflowY:"hidden"},tr),nr)}
@@ -0,0 +1,22 @@
1
+ import { type ReactElement } from "react";
2
+ import type { BannerLine } from "../banner.ts";
3
+ import type { HistoryItem, LiveState } from "./state.ts";
4
+ export interface TranscriptViewportProps {
5
+ readonly width: number;
6
+ readonly history: readonly HistoryItem[];
7
+ readonly live: LiveState;
8
+ readonly banner?: readonly BannerLine[];
9
+ readonly animateBanner: boolean;
10
+ readonly onBannerSettled: () => void;
11
+ readonly navigationBlocked: boolean;
12
+ }
13
+ interface VisibleWindow {
14
+ readonly startIndex: number;
15
+ readonly endIndex: number;
16
+ readonly topSpacer: number;
17
+ readonly bottomSpacer: number;
18
+ readonly totalHeight: number;
19
+ }
20
+ export declare function computeVisibleWindow(heights: readonly number[], viewportHeight: number, scrollOffset: number): VisibleWindow;
21
+ export declare function TranscriptViewport(props: TranscriptViewportProps): ReactElement;
22
+ export {};
@@ -0,0 +1 @@
1
+ import{createElement as e,useCallback as t,useEffect as n,useMemo as i,useRef as o,useState as r}from"react";import{Box as a,Text as l,useBoxMetrics as c,useInput as h,useStdout as s}from"ink";import{BannerHistoryView as m,BannerLinesView as d}from"./banner-view.js";import{HistoryItemView as p}from"./history-item.js";import{LiveRegion as g}from"./live-region.js";import{DISABLE_MOUSE_TRACKING as u,ENABLE_MOUSE_TRACKING as f,parseMouseWheelInput as v}from"./mouse-input.js";const x=20,y=3;function k(e){return e.streamingText.length>0||e.reasoningText.length>0||e.activeTools.length>0||e.compacting}function M(t,n){const i="user"===t.kind||"assistant"===t.kind||"reasoning"===t.kind||"reasoning"===n?.kind,o="tool"===t.kind||"denied"===t.kind||"cancelled"===t.kind;return{key:`history:${t.id}`,element:e(p,{item:t}),paddingTop:i?1:0,marginLeft:o?3:1}}export function computeVisibleWindow(e,t,n){const i=Math.max(1,Math.floor(t)),o=e.reduce((e,t)=>e+t,0),r=Math.max(0,o-i),a=Math.min(Math.max(0,Math.floor(n)),r),l=Math.max(0,o-i-a),c=Math.max(0,l-i),h=Math.min(o,l+2*i);let s=0,m=0;for(;m<e.length&&s+(e[m]??0)<=c;)s+=e[m]??0,m+=1;const d=s;let p=m;for(;p<e.length&&s<h;)s+=e[p]??0,p+=1;return{startIndex:m,endIndex:p,topSpacer:d,bottomSpacer:Math.max(0,o-s),totalHeight:o}}function w({entry:t,onMeasure:i}){const r=o(null);return n(()=>{const e=r.current;if(null!==e){const n=e.yogaNode?.getComputedLayout();void 0!==n&&i(t.key,n.height)}}),e(a,{ref:r,flexDirection:"column",flexShrink:0,paddingTop:t.paddingTop,marginLeft:t.marginLeft},t.element)}export function TranscriptViewport(p){const{stdout:y}=s(),S=o(null),b=c(S),[T,H]=r(()=>new Map),[L,$]=r(0),[j,B]=r(0),[C,D]=r(!1),E=o(0);E.current=L;const I=i(()=>{const t=p.history.map((e,t)=>M(e,p.history[t-1]));return void 0!==p.banner&&t.unshift({key:"animated-banner",element:p.animateBanner?e(m,{lines:p.banner,onSettled:p.onBannerSettled}):e(d,{lines:p.banner}),paddingTop:0,marginLeft:1}),k(p.live)&&t.push({key:"live",element:e(g,{live:p.live}),paddingTop:0,marginLeft:1}),t},[p.animateBanner,p.banner,p.history,p.live,p.onBannerSettled]);n(()=>{H(new Map)},[p.width]);const V=t((e,t)=>{H(n=>{if(n.get(e)===t)return n;const i=new Map(n);return i.set(e,t),i})},[]);n(()=>{const e=new Set(I.map(e=>e.key));H(t=>[...t.keys()].every(t=>e.has(t))?t:new Map([...t].filter(([t])=>e.has(t))))},[I]);const G=I.map(e=>T.get(e.key)),W=G.every(e=>void 0!==e),A=W?G.map(e=>e??0):[],N=A.reduce((e,t)=>e+t,0),U=Math.max(1,b.height),Y=Math.max(0,N-U),q=Math.min(L,Y),z=o(void 0);n(()=>{if(!W)return;const e=z.current;if(z.current={totalHeight:N,viewportHeight:U},void 0===e||0===E.current)return;const t=N-e.totalHeight-(U-e.viewportHeight);0!==t&&$(e=>Math.min(Math.max(0,e+t),Y))},[W,Y,N,U]);const F=o(p.history.length);n(()=>{const e=F.current;F.current=p.history.length,E.current>0&&p.history.length>e&&B(t=>t+p.history.length-e)},[p.history.length]);const J=`${p.live.streamingText.length}:${p.live.reasoningText.length}:${p.live.activeTools.length}:${p.live.compacting?"1":"0"}`,K=o(J);n(()=>{E.current>0&&K.current!==J&&D(!0),K.current=J},[J]);const O=t(()=>{B(0),D(!1)},[]),P=t(e=>{const t=Math.min(Math.max(0,e),Y);$(t),0===t&&O()},[O,Y]),Q=t(e=>{P(E.current+e)},[P]),R=t(()=>{H(new Map),P(0)},[P]);h((e,t)=>{const n=v(e);if(void 0!==n){const e=n.row-1;return void(b.hasMeasured&&e>=b.top&&e<b.top+b.height&&Q("up"===n.direction?3:-3))}t.pageUp?Q(Math.max(1,U-2)):t.pageDown?Q(-Math.max(1,U-2)):t.ctrl&&t.home?P(Y):t.ctrl&&t.end&&R()},{isActive:!p.navigationBlocked}),n(()=>(y.write(f),()=>{y.write(u)}),[y]);const X=W&&I.length>x?computeVisibleWindow(A,U,q):void 0,Z=void 0===X?I:I.slice(X.startIndex,X.endIndex),_=X?.totalHeight??(W?N:void 0),ee=[];void 0!==X&&X.topSpacer>0&&ee.push(e(a,{key:"top-spacer",height:X.topSpacer,flexShrink:0}));for(const t of Z)ee.push(e(w,{key:t.key,entry:t,onMeasure:V}));void 0!==X&&X.bottomSpacer>0&&ee.push(e(a,{key:"bottom-spacer",height:X.bottomSpacer,flexShrink:0}));const te=q>0&&(j>0||C)?j>0?`↓ ${String(j)} 条新内容 · Ctrl+End 返回底部`:"↓ 有新内容 · Ctrl+End 返回底部":q>0?"↓ Ctrl+End 返回底部":void 0;return e(a,{flexDirection:"column",flexGrow:1,flexShrink:1,minHeight:1},e(a,{ref:S,flexDirection:"column",flexGrow:1,flexShrink:1,minHeight:1,overflowY:"hidden",justifyContent:"flex-end"},e(a,{flexDirection:"column",flexShrink:0,position:"relative",top:q,...void 0===_?{}:{height:_}},...ee)),void 0===te?null:e(a,{flexShrink:0,paddingLeft:1},e(l,{color:"cyan"},te)))}
@@ -9,6 +9,7 @@ export interface TurnStatusLayout {
9
9
  }
10
10
  export declare function composeTurnStatusLayout(activity: TurnActivity, phaseElapsedMs: number, turnElapsedMs: number, width: number): TurnStatusLayout;
11
11
  export declare function composeTurnStatusLine(activity: TurnActivity, phaseElapsedMs: number, turnElapsedMs: number, width: number): string;
12
- export declare function TurnStatusLine({ activity }: {
13
- activity: TurnActivity;
12
+ export declare function TurnStatusLine({ activity, width, }: {
13
+ readonly activity: TurnActivity;
14
+ readonly width: number;
14
15
  }): ReactElement;
@@ -1 +1 @@
1
- import{createElement as t,useEffect as e,useRef as n,useState as r}from"react";import{Box as o,Text as a,useStdout as i}from"ink";import{displayWidth as l}from"./display-width.js";import{Spinner as s}from"./spinner.js";import{TURN_ACTIVITY_KINDS as u}from"./turn-activity.js";const m=250,c=2,p=2,d=2,h=4,f=1e3,g="…",y=new Intl.Segmenter(void 0,{granularity:"grapheme"}),T={[u.waitingModel]:"gray",[u.reasoning]:"magenta",[u.replying]:"green",[u.tool]:"cyan",[u.compacting]:"yellow",[u.waitingUser]:"yellow",[u.cancelling]:"yellow"};function v(t,e){if(e<=0)return"";if(l(t)<=e)return t;const n=e-l(g);let r="",o=0;for(const{segment:e}of y.segment(t)){const t=l(e);if(o+t>n)break;r+=e,o+=t}return`${r}${g}`}export function formatElapsed(t){const e=Math.max(0,Math.floor(t/1e3)),n=Math.floor(e/60),r=e%60;return n>0?`${String(n)}m${String(r)}s`:`${String(r)}s`}export function composeTurnStatusLayout(t,e,n,r){if(r<=0)return{label:"",phaseTime:void 0,spacer:"",turnTime:""};const o=`本轮 ${formatElapsed(n)}`,a=l(o);if(a>=r)return{label:"",phaseTime:void 0,spacer:"",turnTime:v(o,r)};const i=Math.max(0,r-a-2);let s=t.showPhaseElapsed&&e>=f?formatElapsed(e):void 0;let u=i-(void 0===s?0:l(` ${s}`));void 0!==s&&u<4&&(s=void 0,u=i);const m=v(t.label,Math.max(0,u)),c=void 0===s?"":` ${s}`;return{label:m,phaseTime:s,spacer:" ".repeat(Math.max(1,r-l(m)-l(c)-a)),turnTime:o}}export function composeTurnStatusLine(t,e,n,r){const o=composeTurnStatusLayout(t,e,n,r),a=void 0===o.phaseTime?"":` ${o.phaseTime}`;return`${o.label}${a}${o.spacer}${o.turnTime}`}export function TurnStatusLine({activity:l}){const{stdout:u}=i(),c=u.columns||80,p=Date.now(),d=n(p),h=n({key:l.key,startedAt:p});h.current.key!==l.key&&(h.current={key:l.key,startedAt:p});const[f,g]=r(p);e(()=>{const t=setInterval(()=>{g(Date.now())},m);return()=>{clearInterval(t)}},[]);const y=Math.max(f,p),v=composeTurnStatusLayout(l,y-h.current.startedAt,y-d.current,Math.max(0,c-2-2));return t(o,{width:c,paddingX:1},l.animated?t(s,{color:T[l.kind]}):t(a,{color:T[l.kind]},"◆"),c>3?t(a,null," "):null,v.label.length>0?t(a,{color:T[l.kind]},v.label):null,void 0===v.phaseTime?null:t(a,{dimColor:!0},` ${v.phaseTime}`),v.spacer.length>0?t(a,null,v.spacer):null,v.turnTime.length>0?t(a,{dimColor:!0},v.turnTime):null)}
1
+ import{createElement as t,useEffect as e,useRef as n,useState as r}from"react";import{Box as o,Text as a}from"ink";import{displayWidth as i}from"./display-width.js";import{Spinner as l}from"./spinner.js";import{TURN_ACTIVITY_KINDS as s}from"./turn-activity.js";const m=250,u=2,c=2,p=2,d=4,h=1e3,f="…",g=new Intl.Segmenter(void 0,{granularity:"grapheme"}),y={[s.waitingModel]:"gray",[s.reasoning]:"magenta",[s.replying]:"green",[s.tool]:"cyan",[s.compacting]:"yellow",[s.waitingUser]:"yellow",[s.cancelling]:"yellow"};function T(t,e){if(e<=0)return"";if(i(t)<=e)return t;const n=e-i(f);let r="",o=0;for(const{segment:e}of g.segment(t)){const t=i(e);if(o+t>n)break;r+=e,o+=t}return`${r}${f}`}export function formatElapsed(t){const e=Math.max(0,Math.floor(t/1e3)),n=Math.floor(e/60),r=e%60;return n>0?`${String(n)}m${String(r)}s`:`${String(r)}s`}export function composeTurnStatusLayout(t,e,n,r){if(r<=0)return{label:"",phaseTime:void 0,spacer:"",turnTime:""};const o=`本轮 ${formatElapsed(n)}`,a=i(o);if(a>=r)return{label:"",phaseTime:void 0,spacer:"",turnTime:T(o,r)};const l=Math.max(0,r-a-2);let s=t.showPhaseElapsed&&e>=h?formatElapsed(e):void 0;let m=l-(void 0===s?0:i(` ${s}`));void 0!==s&&m<4&&(s=void 0,m=l);const u=T(t.label,Math.max(0,m)),c=void 0===s?"":` ${s}`;return{label:u,phaseTime:s,spacer:" ".repeat(Math.max(1,r-i(u)-i(c)-a)),turnTime:o}}export function composeTurnStatusLine(t,e,n,r){const o=composeTurnStatusLayout(t,e,n,r),a=void 0===o.phaseTime?"":` ${o.phaseTime}`;return`${o.label}${a}${o.spacer}${o.turnTime}`}export function TurnStatusLine({activity:i,width:s}){const u=Date.now(),c=n(u),p=n({key:i.key,startedAt:u});p.current.key!==i.key&&(p.current={key:i.key,startedAt:u});const[d,h]=r(u);e(()=>{const t=setInterval(()=>{h(Date.now())},m);return()=>{clearInterval(t)}},[]);const f=Math.max(d,u),g=composeTurnStatusLayout(i,f-p.current.startedAt,f-c.current,Math.max(0,s-2-2));return t(o,{width:s,paddingX:1},i.animated?t(l,{color:y[i.kind]}):t(a,{color:y[i.kind]},"◆"),s>3?t(a,null," "):null,g.label.length>0?t(a,{color:y[i.kind]},g.label):null,void 0===g.phaseTime?null:t(a,{dimColor:!0},` ${g.phaseTime}`),g.spacer.length>0?t(a,null,g.spacer):null,g.turnTime.length>0?t(a,{dimColor:!0},g.turnTime):null)}
@@ -0,0 +1,66 @@
1
+ import { type ChatScreenMode } from "../../config/schema.ts";
2
+ export declare const CHAT_PRESENTATIONS: {
3
+ readonly fullscreen: "fullscreen";
4
+ readonly inline: "inline";
5
+ };
6
+ export type ChatPresentation = (typeof CHAT_PRESENTATIONS)[keyof typeof CHAT_PRESENTATIONS];
7
+ export declare const CHAT_MULTIPLEXERS: {
8
+ readonly none: "none";
9
+ readonly tmux: "tmux";
10
+ readonly tmuxControl: "tmux-control";
11
+ readonly tmuxUnknown: "tmux-unknown";
12
+ readonly zellij: "zellij";
13
+ };
14
+ export type ChatMultiplexer = (typeof CHAT_MULTIPLEXERS)[keyof typeof CHAT_MULTIPLEXERS];
15
+ export type ChatScreenModeSource = "cli" | "config";
16
+ export interface ChatTerminalCapabilities {
17
+ readonly stdinIsTty: boolean;
18
+ readonly stdoutIsTty: boolean;
19
+ readonly rawModeSupported: boolean;
20
+ readonly interactive: boolean;
21
+ readonly ci: boolean;
22
+ readonly dumbTerminal: boolean;
23
+ readonly screenReader: boolean;
24
+ readonly multiplexer: ChatMultiplexer;
25
+ }
26
+ export interface DetectChatTerminalCapabilitiesInput {
27
+ readonly stdinIsTty: boolean;
28
+ readonly stdoutIsTty: boolean;
29
+ readonly rawModeSupported: boolean;
30
+ readonly env: NodeJS.ProcessEnv;
31
+ readonly readTmuxClientFlags?: () => Promise<string | undefined>;
32
+ }
33
+ export interface ResolveChatScreenModeRequestInput {
34
+ readonly configMode: ChatScreenMode;
35
+ readonly cliValue?: string;
36
+ readonly messagePresent: boolean;
37
+ readonly json: boolean;
38
+ readonly server: boolean;
39
+ readonly list: boolean;
40
+ }
41
+ export type ChatScreenModeRequest = {
42
+ readonly ok: true;
43
+ readonly mode: ChatScreenMode;
44
+ readonly source: ChatScreenModeSource;
45
+ } | {
46
+ readonly ok: false;
47
+ readonly error: string;
48
+ };
49
+ export type ChatPresentationDecision = {
50
+ readonly ok: true;
51
+ readonly presentation: ChatPresentation;
52
+ readonly reason: "requested" | "auto" | "non-interactive" | "ci" | "dumb-terminal" | "screen-reader" | "tmux-control" | "tmux-unknown" | "zellij";
53
+ readonly warning?: string;
54
+ } | {
55
+ readonly ok: false;
56
+ readonly error: string;
57
+ };
58
+ export declare function isCiEnvironment(env: NodeJS.ProcessEnv): boolean;
59
+ export declare function readCurrentTmuxClientFlags(): Promise<string | undefined>;
60
+ export declare function detectChatTerminalCapabilities(input: DetectChatTerminalCapabilitiesInput): Promise<ChatTerminalCapabilities>;
61
+ export declare function resolveChatScreenModeRequest(input: ResolveChatScreenModeRequestInput): ChatScreenModeRequest;
62
+ export declare function resolveChatPresentation(input: {
63
+ readonly mode: ChatScreenMode;
64
+ readonly source: ChatScreenModeSource;
65
+ readonly capabilities: ChatTerminalCapabilities;
66
+ }): ChatPresentationDecision;
@@ -0,0 +1 @@
1
+ import{execFile as e}from"node:child_process";import{CHAT_SCREEN_MODES as n,chatScreenModeSchema as t}from"../../config/schema.js";export const CHAT_PRESENTATIONS={fullscreen:"fullscreen",inline:"inline"};export const CHAT_MULTIPLEXERS={none:"none",tmux:"tmux",tmuxControl:"tmux-control",tmuxUnknown:"tmux-unknown",zellij:"zellij"};function i(e,n){const t=e[n];return void 0!==t&&"0"!==t&&"false"!==t}export function isCiEnvironment(e){return i(e,"CI")||i(e,"CONTINUOUS_INTEGRATION")}function o(e){if(void 0===e)return!1;const n=e.split(",").map(e=>e.trim());return n.includes("control")||n.includes("control-mode")}export function readCurrentTmuxClientFlags(){return new Promise(n=>{e("tmux",["display-message","-p","#{client_flags}"],{encoding:"utf8",timeout:250,maxBuffer:4096,windowsHide:!0},(e,t)=>{n(e?void 0:t.trim())})})}export async function detectChatTerminalCapabilities(e){let n=CHAT_MULTIPLEXERS.none;if(e.env.TMUX){const t=await(e.readTmuxClientFlags??readCurrentTmuxClientFlags)();n=void 0===t?CHAT_MULTIPLEXERS.tmuxUnknown:o(t)?CHAT_MULTIPLEXERS.tmuxControl:CHAT_MULTIPLEXERS.tmux}else e.env.ZELLIJ&&(n=CHAT_MULTIPLEXERS.zellij);return{stdinIsTty:e.stdinIsTty,stdoutIsTty:e.stdoutIsTty,rawModeSupported:e.rawModeSupported,interactive:e.stdinIsTty&&e.stdoutIsTty&&e.rawModeSupported,ci:isCiEnvironment(e.env),dumbTerminal:"dumb"===e.env.TERM?.toLowerCase(),screenReader:"true"===e.env.INK_SCREEN_READER,multiplexer:n}}export function resolveChatScreenModeRequest(e){if(void 0===e.cliValue)return{ok:!0,mode:e.configMode,source:"config"};const i=t.safeParse(e.cliValue);if(!i.success)return{ok:!1,error:`--screen-mode 仅支持 ${n.join("、")}`};const o=[e.messagePresent?"message":void 0,e.json?"--json":void 0,e.server?"--server":void 0,e.list?"--list":void 0].filter(e=>void 0!==e);return o.length>0?{ok:!1,error:`--screen-mode 仅适用于无起始消息的交互式 roll chat,不能与 ${o.join("、")} 同用`}:{ok:!0,mode:i.data,source:"cli"}}function r(e){return[e.stdinIsTty?void 0:"stdin TTY",e.stdoutIsTty?void 0:"stdout TTY",e.rawModeSupported?void 0:"raw mode"].filter(e=>void 0!==e).join("、")}export function resolveChatPresentation(e){if("inline"===e.mode)return{ok:!0,presentation:CHAT_PRESENTATIONS.inline,reason:"requested"};if("fullscreen"===e.mode){if(e.capabilities.interactive)return{ok:!0,presentation:CHAT_PRESENTATIONS.fullscreen,reason:"requested"};const n=r(e.capabilities);return"cli"===e.source?{ok:!1,error:`--screen-mode fullscreen 需要交互式终端,当前缺少:${n}`}:{ok:!0,presentation:CHAT_PRESENTATIONS.inline,reason:"non-interactive",warning:`chat.screen-mode=fullscreen 需要交互式终端,当前缺少 ${n};已回退基础 REPL`}}return e.capabilities.interactive?e.capabilities.ci?{ok:!0,presentation:CHAT_PRESENTATIONS.inline,reason:"ci"}:e.capabilities.dumbTerminal?{ok:!0,presentation:CHAT_PRESENTATIONS.inline,reason:"dumb-terminal"}:e.capabilities.screenReader?{ok:!0,presentation:CHAT_PRESENTATIONS.inline,reason:"screen-reader"}:e.capabilities.multiplexer===CHAT_MULTIPLEXERS.tmuxControl?{ok:!0,presentation:CHAT_PRESENTATIONS.inline,reason:"tmux-control"}:e.capabilities.multiplexer===CHAT_MULTIPLEXERS.tmuxUnknown?{ok:!0,presentation:CHAT_PRESENTATIONS.inline,reason:"tmux-unknown"}:e.capabilities.multiplexer===CHAT_MULTIPLEXERS.zellij?{ok:!0,presentation:CHAT_PRESENTATIONS.inline,reason:"zellij"}:{ok:!0,presentation:CHAT_PRESENTATIONS.fullscreen,reason:"auto"}:{ok:!0,presentation:CHAT_PRESENTATIONS.inline,reason:"non-interactive"}}
@@ -15,13 +15,13 @@ declare const BrowserStopToolResultSchema: z.ZodObject<{
15
15
  }, "strip", z.ZodTypeAny, {
16
16
  status: "stopped" | "failed" | "not_running" | "not_found";
17
17
  browserInstance: string;
18
- mode?: "managed-cdp" | "remote-cdp" | "existing-session" | undefined;
19
18
  message?: string | undefined;
19
+ mode?: "managed-cdp" | "remote-cdp" | "existing-session" | undefined;
20
20
  }, {
21
21
  status: "stopped" | "failed" | "not_running" | "not_found";
22
22
  browserInstance: string;
23
- mode?: "managed-cdp" | "remote-cdp" | "existing-session" | undefined;
24
23
  message?: string | undefined;
24
+ mode?: "managed-cdp" | "remote-cdp" | "existing-session" | undefined;
25
25
  }>, "many">;
26
26
  }, "strip", z.ZodTypeAny, {
27
27
  stopped: number;
@@ -29,8 +29,8 @@ declare const BrowserStopToolResultSchema: z.ZodObject<{
29
29
  results: {
30
30
  status: "stopped" | "failed" | "not_running" | "not_found";
31
31
  browserInstance: string;
32
- mode?: "managed-cdp" | "remote-cdp" | "existing-session" | undefined;
33
32
  message?: string | undefined;
33
+ mode?: "managed-cdp" | "remote-cdp" | "existing-session" | undefined;
34
34
  }[];
35
35
  }, {
36
36
  stopped: number;
@@ -38,8 +38,8 @@ declare const BrowserStopToolResultSchema: z.ZodObject<{
38
38
  results: {
39
39
  status: "stopped" | "failed" | "not_running" | "not_found";
40
40
  browserInstance: string;
41
- mode?: "managed-cdp" | "remote-cdp" | "existing-session" | undefined;
42
41
  message?: string | undefined;
42
+ mode?: "managed-cdp" | "remote-cdp" | "existing-session" | undefined;
43
43
  }[];
44
44
  }>;
45
45
  type BrowserStopToolResult = z.infer<typeof BrowserStopToolResultSchema>;
@@ -1,7 +1,7 @@
1
1
  import type { AgentSession } from "@roll-agent/runtime";
2
2
  import { type ChatConfirm } from "../utils/chat-renderer.ts";
3
3
  import type { ChatCommandResult } from "../../types/chat.ts";
4
- import type { RollConfig } from "../../config/schema.ts";
4
+ import { type RollConfig } from "../../config/schema.ts";
5
5
  import type { LlmConfigReadiness } from "../../config/helpers.ts";
6
6
  type RuntimeModule = typeof import("@roll-agent/runtime");
7
7
  type ThreadStoreInstance = InstanceType<RuntimeModule["ThreadStore"]>;
@@ -71,5 +71,9 @@ declare const _default: import("citty").CommandDef<{
71
71
  description: string;
72
72
  default: false;
73
73
  };
74
+ "screen-mode": {
75
+ type: "string";
76
+ description: string;
77
+ };
74
78
  }>;
75
79
  export default _default;
@@ -1 +1 @@
1
- var e=this&&this.__rewriteRelativeImportExtension||function(e,t){return"string"==typeof e&&/^\.\.?\//.test(e)?e.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i,function(e,o,n,s,r){return o?t?".jsx":".js":!n||s&&r?n+s+"."+r.toLowerCase()+"js":e}):e};import{defineCommand as t}from"citty";import{inspectLlmConfigReadiness as o}from"../../config/helpers.js";import{loadConfig as n}from"../../config/loader.js";import{resolveLLMCall as s,thinkingProviderOptions as r}from"../../llm/providers.js";import{createInterface as i}from"node:readline/promises";import a from"chalk";import c from"cli-table3";import{isDebugLogEnabled as u,log as l}from"../utils/output.js";import{ChatRenderer as d,clackConfirm as p}from"../utils/chat-renderer.js";import{titleFromMessage as m}from"../chat/title.js";import{buildBannerLines as g,renderBannerText as f}from"../chat/banner.js";import{getCurrentVersion as h}from"../utils/update-checker.js";import{formatSkillList as v,parseSkillInvocation as C}from"../chat/ink/commands.js";import{installCurrentCliShim as S}from"../utils/current-cli-shim.js";const w=import.meta.url.endsWith(".ts")?"ts":"js";function y(e,t){return new e.ConfigurableToolPolicy({defaultMode:t.runtime.approval.default,overrides:t.runtime.approval.overrides})}export const CHAT_ENGINE_SURFACES={ink:"ink",basicRepl:"basic-repl",oneShot:"one-shot",json:"json",server:"server"};const k={[CHAT_ENGINE_SURFACES.ink]:"interactive",[CHAT_ENGINE_SURFACES.basicRepl]:"interactive",[CHAT_ENGINE_SURFACES.oneShot]:"one-shot",[CHAT_ENGINE_SURFACES.json]:"one-shot",[CHAT_ENGINE_SURFACES.server]:"server"};export function chatHostModeForSurface(e){return k[e]}function E(e){console.log(JSON.stringify(e,null,2))}function O(e){l.warn(`Agent "${e.agentName}" 启动失败:${e.message}`)}function T(e){l.warn(`skill 目录加载警告:${e}`)}function N(){const e={...process.env};try{const t=S({env:e});return{env:e,dispose:()=>t.dispose()}}catch(t){return l.warn(`无法锁定当前 Roll CLI,子任务将继续按 PATH 查找 roll:${t instanceof Error?t.message:String(t)}`),{env:e,dispose:()=>{}}}}export function createChatEngine(e){return new e.runtime.ConversationEngine({config:e.config,model:e.model,store:e.store,hostMode:chatHostModeForSurface(e.surface),policy:y(e.runtime,e.config),maxSteps:e.config.runtime.maxSteps,...e.providerOptions?{providerOptions:e.providerOptions}:{},...e.structuredOutputProviderOptions?{structuredOutputProviderOptions:e.structuredOutputProviderOptions}:{},...e.structuredOutputReasoning?{structuredOutputReasoning:e.structuredOutputReasoning}:{},debugEvents:u(),onAgentBootstrapIssue:O,onSkillLibraryIssue:T,...e.shellEnv?{shellEnv:e.shellEnv}:{}})}async function b(e,t,o){l.debug(`chat.repl input waiting · ${o}`),e.resume();try{const n=await e.question(t);return l.debug(`chat.repl input received · ${o} · chars=${String(n.length)}`),n}catch(e){const t=e instanceof Error?e.message:String(e);return void l.debug(`chat.repl input closed · ${o} · ${t}`)}finally{e.pause()}}async function R(){return import("@roll-agent/runtime")}async function I(t,o){const n=new URL(`./setup.${w}`,import.meta.url).href;return(await import(e(n))).runChatOnboarding({},{provider:t,model:o})}export function resolveChatLlmReadiness(e){return o(e,{provider:e.runtime.provider??e.llm.defaultProvider,model:e.runtime.model??e.llm.defaultModel})}export function resolveChatLlmCalls(e,t,o,n,r,i=void 0,a=!0){const c=s(e,t,o,"chat",n,r),u=a?s(e,t,o,"structured-output",n,i??r):void 0;return{model:c.model,...c.providerOptions?{providerOptions:c.providerOptions}:{},...u?.providerOptions?{structuredOutputProviderOptions:u.providerOptions}:{},...u?.reasoning?{structuredOutputReasoning:u.reasoning}:{}}}async function x(e){const t=resolveChatLlmReadiness(e);if(!t.configured||!t.providerConfig)return l.error(t.message),void(process.exitCode=1);const o=t.providerConfig,n=t.provider,s=t.model,r=await R(),{ThreadStore:i,RuntimeServer:a,createStdioConnection:c}=r,{model:u,providerOptions:d,structuredOutputProviderOptions:p,structuredOutputReasoning:m}=resolveChatLlmCalls(n,s,o.apiKey,o.baseUrl,e.runtime.thinkingLevel,e.runtime.compaction.thinkingLevel,"summarize"===e.runtime.compaction.strategy),g=new i(e.runtime.threadsDir),f=N();let h;try{h=createChatEngine({runtime:r,config:e,model:u,store:g,surface:CHAT_ENGINE_SURFACES.server,shellEnv:f.env,...d?{providerOptions:d}:{},...p?{structuredOutputProviderOptions:p}:{},...m?{structuredOutputReasoning:m}:{}});const t=h,o=c(process.stdin,process.stdout),n=new a(t,o);o.onClose(()=>{n.abortAll().then(()=>t.dispose()).catch(()=>{}).finally(()=>{f.dispose(),g.close(),process.exit(0)})})}catch(e){throw await(h?.dispose().catch(()=>{})),f.dispose(),g.close(),e}l.info("roll runtime-server 已启动(stdio JSON-RPC,等待客户端连接)")}async function A(e,t){const{ThreadStore:o}=await R(),n=new o(e.runtime.threadsDir);try{const e=n.listThreads();if(t){const t=e.map(e=>({...e,messageCount:n.countMessages(e.id)}));return void console.log(JSON.stringify(t,null,2))}if(0===e.length)return void console.log('暂无会话。用 `roll chat "<message>"` 开始一个。');const o=new c({head:["Session ID","标题","消息","更新时间"],style:{head:["cyan"]}});for(const t of e)o.push([t.id,t.title??"-",String(n.countMessages(t.id)),t.updatedAt]);console.log(o.toString())}finally{n.close()}}export async function runJsonTurn(e,t){const o=[],n=[],s=[],r=[];let i,a,c,u,l="";for await(const d of e.send(t))switch(d.type){case"text-delta":l+=d.delta;break;case"tool-call":o.push({summary:`${d.agentName}.${d.toolName}`,agentName:d.agentName,toolName:d.toolName});break;case"confirmation-required":r.push({summary:`${d.agentName}.${d.toolName}`,agentName:d.agentName,toolName:d.toolName}),e.reject(d.approvalId,"json 模式不支持交互确认");break;case"step-finish":n.push({finishReason:d.finishReason,...d.usage?{usage:d.usage}:{}});break;case"message-finish":a=d.totalUsage,c=d.sessionUsage,u=d.contextInputTokens;break;case"context-compacted":s.push({reason:d.reason,strategy:d.strategy,removed:d.removed,kept:d.kept,...void 0!==d.truncatedTools?{truncatedTools:d.truncatedTools}:{},...void 0!==d.beforeInputTokens?{beforeInputTokens:d.beforeInputTokens}:{},...void 0!==d.checkpointId?{checkpointId:d.checkpointId}:{},...void 0!==d.checkpointGeneration?{checkpointGeneration:d.checkpointGeneration}:{},...void 0!==d.checkpointSummaryStatus?{checkpointSummaryStatus:d.checkpointSummaryStatus}:{}});break;case"turn-cancelled":case"error":i=d.message}const d=e.getContextWindow();return void 0!==i?{status:"failed",stage:"execute",message:i,sessionId:e.id}:r.length>0?{status:"needs_confirmation",sessionId:e.id,message:"存在需要确认的工具调用,请在交互模式下执行或显式批准",pendingActions:r}:{status:"completed",sessionId:e.id,output:l,steps:o,...n.length>0?{stepUsages:n}:{},...a?{totalUsage:a}:{},...c?{sessionUsage:c}:{},...void 0!==d?{contextWindow:d}:{},...void 0!==u?{contextInputTokens:u}:{},...s.length>0?{compactions:s}:{}}}export async function runRepl(e,t,o,n={input:process.stdin,output:process.stdout}){const s=i({input:n.input,output:n.output});s.on("SIGINT",()=>s.close());const r=n.confirm??(async e=>{l.debug("chat.repl input waiting · confirm"),s.pause();const t=await p(e);return l.debug(`chat.repl input received · confirm · approved=${String(t)}`),t}),c=new d(r,e.getContextWindow()),u=e.getSkillSummaries();l.info("进入多轮对话(输入 exit / quit 或 Ctrl-C 退出,/compact 手动压缩上下文)");let g=!o,f=!1;try{for(;;){const o=await b(s,a.green("› "),"prompt");if(void 0===o)break;const n=o.trim();if(0===n.length)continue;if("exit"===n||"quit"===n)break;if("/compact"===n){l.debug("chat.repl manual compact requested");for await(const t of e.compact("manual"))await c.handle(t,e);l.debug("chat.repl manual compact completed");continue}if("/skills"===n){l.info(v(u,(process.stdout.columns||96)-2));continue}const r=C(n,u);if(r&&0===r.prompt.length)l.info("用法: /<skill-name> [/<skill-name> ...] 你的请求");else{g||(t.updateTitle(e.id,m(n)),g=!0),f=!0,l.debug(`chat.repl send start · chars=${String(n.length)}`);for await(const t of e.send(n))await c.handle(t,e);l.debug("chat.repl send completed")}}}finally{s.close(),o&&!f&&0===t.countMessages(e.id)&&t.deleteThread(e.id)}}export default t({meta:{description:"会话式 AI 助手(多轮对话 + 自动工具调用)"},args:{message:{type:"positional",description:"起始消息",required:!1},session:{type:"string",description:"继续已有会话的 session ID"},last:{type:"boolean",description:"继续最近一个会话",default:!1},list:{type:"boolean",description:"列出已有会话",default:!1},json:{type:"boolean",description:"JSON 格式输出",default:!1},server:{type:"boolean",description:"以 JSON-RPC daemon 模式运行(stdio,供 GUI/前端接入)",default:!1}},async run({args:e}){let{config:t}=n();if(e.server)return void await x(t);if(e.list)return void await A(t,e.json);let o=resolveChatLlmReadiness(t);if(!o.configured){!0===process.stdin.isTTY&&!0===process.stderr.isTTY&&!e.json&&await I(o.provider,o.model)&&(t=n().config,o=resolveChatLlmReadiness(t))}if(!o.configured||!o.providerConfig)return l.error(o.message),void(process.exitCode=1);const s=o.providerConfig,i=o.provider,a=o.model;if(e.json&&!e.message)return l.error('--json 模式需要消息:roll chat "<message>" --json'),void(process.exitCode=1);const c=await R(),{ThreadStore:u}=c,{model:v,providerOptions:C,structuredOutputProviderOptions:S,structuredOutputReasoning:y}=resolveChatLlmCalls(i,a,s.apiKey,s.baseUrl,t.runtime.thinkingLevel,t.runtime.compaction.thinkingLevel,"summarize"===t.runtime.compaction.strategy),k=new u(t.runtime.threadsDir),O=Boolean(process.stdout.isTTY&&process.stdin.isTTY&&"function"==typeof process.stdin.setRawMode),T=e.message?e.json?CHAT_ENGINE_SURFACES.json:CHAT_ENGINE_SURFACES.oneShot:O?CHAT_ENGINE_SURFACES.ink:CHAT_ENGINE_SURFACES.basicRepl,b=N();let j,_;try{let o;if(j=createChatEngine({runtime:c,config:t,model:v,store:k,surface:T,shellEnv:b.env,...C?{providerOptions:C}:{},...S?{structuredOutputProviderOptions:S}:{},...y?{structuredOutputReasoning:y}:{}}),e.session)o=await j.resumeSession(e.session);else if(e.last){const e=k.listThreads()[0];if(!e)return l.error('暂无可继续的会话,先用 `roll chat "<message>"` 开始一个'),void(process.exitCode=1);o=await j.resumeSession(e.id)}else o=await j.createSession(e.message?{title:m(e.message)}:{});if(_=o,e.json&&e.message){const t=await runJsonTurn(o,e.message);return E(t),void("completed"!==t.status&&(process.exitCode=1))}if(l.info(`会话 ${o.id}`),t.runtime.compaction.enabled&&void 0===o.getContextWindow()&&l.warn(`未知模型 "${a}" 的 context window,阈值自动压缩不可用。可在 roll.config.yaml 设置 runtime.context-window`),e.message){const t=new d(p,o.getContextWindow());for await(const n of o.send(e.message))await t.handle(n,o)}else{const n=!e.session&&!e.last,s=await j.getContextSummary(),c={version:h(),model:a,agentCount:s.agentCount,skillCount:s.skillCount};let u=!1;if(O)try{const e=new URL(`../chat/ink/run-ink-repl.${w}`,import.meta.url).href,{runInkRepl:s}=await import(e);await s(o,k,n,{model:a,banner:c,initialThinkingLevel:t.runtime.thinkingLevel,onThinkingChange:e=>o.setProviderOptions(r(i,a,e))}),u=!0}catch(e){l.warn(`Ink TUI 不可用,回退到基础多轮模式:${e instanceof Error?e.message:String(e)}`)}u||(process.stderr.write(`${f(g(c,process.stdout.columns||80))}\n`),await runRepl(o,k,n))}}catch(e){l.error(e instanceof Error?e.message:String(e)),process.exitCode=1}finally{await(_?.close()),await(j?.dispose()),k.close(),b.dispose()}}});
1
+ var e=this&&this.__rewriteRelativeImportExtension||function(e,t){return"string"==typeof e&&/^\.\.?\//.test(e)?e.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i,function(e,o,s,n,r){return o?t?".jsx":".js":!s||n&&r?s+n+"."+r.toLowerCase()+"js":e}):e};import{defineCommand as t}from"citty";import{inspectLlmConfigReadiness as o}from"../../config/helpers.js";import{loadConfig as s}from"../../config/loader.js";import{resolveLLMCall as n,thinkingProviderOptions as r}from"../../llm/providers.js";import{createInterface as i}from"node:readline/promises";import a from"chalk";import c from"cli-table3";import{isDebugLogEnabled as u,log as d}from"../utils/output.js";import{ChatRenderer as l,clackConfirm as p}from"../utils/chat-renderer.js";import{CHAT_SCREEN_MODES as m}from"../../config/schema.js";import{titleFromMessage as g}from"../chat/title.js";import{buildBannerLines as f,renderBannerText as h}from"../chat/banner.js";import{CHAT_PRESENTATIONS as v,detectChatTerminalCapabilities as C,resolveChatPresentation as w,resolveChatScreenModeRequest as S}from"../chat/screen-mode.js";import{getCurrentVersion as y}from"../utils/update-checker.js";import{formatSkillList as k,parseSkillInvocation as E}from"../chat/ink/commands.js";import{installCurrentCliShim as T}from"../utils/current-cli-shim.js";const O=import.meta.url.endsWith(".ts")?"ts":"js";function b(e,t){return new e.ConfigurableToolPolicy({defaultMode:t.runtime.approval.default,overrides:t.runtime.approval.overrides})}export const CHAT_ENGINE_SURFACES={ink:"ink",basicRepl:"basic-repl",oneShot:"one-shot",json:"json",server:"server"};const N={[CHAT_ENGINE_SURFACES.ink]:"interactive",[CHAT_ENGINE_SURFACES.basicRepl]:"interactive",[CHAT_ENGINE_SURFACES.oneShot]:"one-shot",[CHAT_ENGINE_SURFACES.json]:"one-shot",[CHAT_ENGINE_SURFACES.server]:"server"};export function chatHostModeForSurface(e){return N[e]}function R(e){console.log(JSON.stringify(e,null,2))}function I(e){d.warn(`Agent "${e.agentName}" 启动失败:${e.message}`)}function x(e){d.warn(`skill 目录加载警告:${e}`)}function j(){const e={...process.env};try{const t=T({env:e});return{env:e,dispose:()=>t.dispose()}}catch(t){return d.warn(`无法锁定当前 Roll CLI,子任务将继续按 PATH 查找 roll:${t instanceof Error?t.message:String(t)}`),{env:e,dispose:()=>{}}}}export function createChatEngine(e){return new e.runtime.ConversationEngine({config:e.config,model:e.model,store:e.store,hostMode:chatHostModeForSurface(e.surface),policy:b(e.runtime,e.config),maxSteps:e.config.runtime.maxSteps,...e.providerOptions?{providerOptions:e.providerOptions}:{},...e.structuredOutputProviderOptions?{structuredOutputProviderOptions:e.structuredOutputProviderOptions}:{},...e.structuredOutputReasoning?{structuredOutputReasoning:e.structuredOutputReasoning}:{},debugEvents:u(),onAgentBootstrapIssue:I,onSkillLibraryIssue:x,...e.shellEnv?{shellEnv:e.shellEnv}:{}})}async function A(e,t,o){d.debug(`chat.repl input waiting · ${o}`),e.resume();try{const s=await e.question(t);return d.debug(`chat.repl input received · ${o} · chars=${String(s.length)}`),s}catch(e){const t=e instanceof Error?e.message:String(e);return void d.debug(`chat.repl input closed · ${o} · ${t}`)}finally{e.pause()}}async function $(){return import("@roll-agent/runtime")}async function _(t,o){const s=new URL(`./setup.${O}`,import.meta.url).href;return(await import(e(s))).runChatOnboarding({},{provider:t,model:o})}export function resolveChatLlmReadiness(e){return o(e,{provider:e.runtime.provider??e.llm.defaultProvider,model:e.runtime.model??e.llm.defaultModel})}export function resolveChatLlmCalls(e,t,o,s,r,i=void 0,a=!0){const c=n(e,t,o,"chat",s,r),u=a?n(e,t,o,"structured-output",s,i??r):void 0;return{model:c.model,...c.providerOptions?{providerOptions:c.providerOptions}:{},...u?.providerOptions?{structuredOutputProviderOptions:u.providerOptions}:{},...u?.reasoning?{structuredOutputReasoning:u.reasoning}:{}}}async function U(e){const t=resolveChatLlmReadiness(e);if(!t.configured||!t.providerConfig)return d.error(t.message),void(process.exitCode=1);const o=t.providerConfig,s=t.provider,n=t.model,r=await $(),{ThreadStore:i,RuntimeServer:a,createStdioConnection:c}=r,{model:u,providerOptions:l,structuredOutputProviderOptions:p,structuredOutputReasoning:m}=resolveChatLlmCalls(s,n,o.apiKey,o.baseUrl,e.runtime.thinkingLevel,e.runtime.compaction.thinkingLevel,"summarize"===e.runtime.compaction.strategy),g=new i(e.runtime.threadsDir),f=j();let h;try{h=createChatEngine({runtime:r,config:e,model:u,store:g,surface:CHAT_ENGINE_SURFACES.server,shellEnv:f.env,...l?{providerOptions:l}:{},...p?{structuredOutputProviderOptions:p}:{},...m?{structuredOutputReasoning:m}:{}});const t=h,o=c(process.stdin,process.stdout),s=new a(t,o);o.onClose(()=>{s.abortAll().then(()=>t.dispose()).catch(()=>{}).finally(()=>{f.dispose(),g.close(),process.exit(0)})})}catch(e){throw await(h?.dispose().catch(()=>{})),f.dispose(),g.close(),e}d.info("roll runtime-server 已启动(stdio JSON-RPC,等待客户端连接)")}async function L(e,t){const{ThreadStore:o}=await $(),s=new o(e.runtime.threadsDir);try{const e=s.listThreads();if(t){const t=e.map(e=>({...e,messageCount:s.countMessages(e.id)}));return void console.log(JSON.stringify(t,null,2))}if(0===e.length)return void console.log('暂无会话。用 `roll chat "<message>"` 开始一个。');const o=new c({head:["Session ID","标题","消息","更新时间"],style:{head:["cyan"]}});for(const t of e)o.push([t.id,t.title??"-",String(s.countMessages(t.id)),t.updatedAt]);console.log(o.toString())}finally{s.close()}}export async function runJsonTurn(e,t){const o=[],s=[],n=[],r=[];let i,a,c,u,d="";for await(const l of e.send(t))switch(l.type){case"text-delta":d+=l.delta;break;case"tool-call":o.push({summary:`${l.agentName}.${l.toolName}`,agentName:l.agentName,toolName:l.toolName});break;case"confirmation-required":r.push({summary:`${l.agentName}.${l.toolName}`,agentName:l.agentName,toolName:l.toolName}),e.reject(l.approvalId,"json 模式不支持交互确认");break;case"step-finish":s.push({finishReason:l.finishReason,...l.usage?{usage:l.usage}:{}});break;case"message-finish":a=l.totalUsage,c=l.sessionUsage,u=l.contextInputTokens;break;case"context-compacted":n.push({reason:l.reason,strategy:l.strategy,removed:l.removed,kept:l.kept,...void 0!==l.truncatedTools?{truncatedTools:l.truncatedTools}:{},...void 0!==l.beforeInputTokens?{beforeInputTokens:l.beforeInputTokens}:{},...void 0!==l.checkpointId?{checkpointId:l.checkpointId}:{},...void 0!==l.checkpointGeneration?{checkpointGeneration:l.checkpointGeneration}:{},...void 0!==l.checkpointSummaryStatus?{checkpointSummaryStatus:l.checkpointSummaryStatus}:{}});break;case"turn-cancelled":case"error":i=l.message}const l=e.getContextWindow();return void 0!==i?{status:"failed",stage:"execute",message:i,sessionId:e.id}:r.length>0?{status:"needs_confirmation",sessionId:e.id,message:"存在需要确认的工具调用,请在交互模式下执行或显式批准",pendingActions:r}:{status:"completed",sessionId:e.id,output:d,steps:o,...s.length>0?{stepUsages:s}:{},...a?{totalUsage:a}:{},...c?{sessionUsage:c}:{},...void 0!==l?{contextWindow:l}:{},...void 0!==u?{contextInputTokens:u}:{},...n.length>0?{compactions:n}:{}}}export async function runRepl(e,t,o,s={input:process.stdin,output:process.stdout}){const n=i({input:s.input,output:s.output});n.on("SIGINT",()=>n.close());const r=s.confirm??(async e=>{d.debug("chat.repl input waiting · confirm"),n.pause();const t=await p(e);return d.debug(`chat.repl input received · confirm · approved=${String(t)}`),t}),c=new l(r,e.getContextWindow()),u=e.getSkillSummaries();d.info("进入多轮对话(输入 exit / quit 或 Ctrl-C 退出,/compact 手动压缩上下文)");let m=!o,f=!1;try{for(;;){const o=await A(n,a.green("› "),"prompt");if(void 0===o)break;const s=o.trim();if(0===s.length)continue;if("exit"===s||"quit"===s)break;if("/compact"===s){d.debug("chat.repl manual compact requested");for await(const t of e.compact("manual"))await c.handle(t,e);d.debug("chat.repl manual compact completed");continue}if("/skills"===s){d.info(k(u,(process.stdout.columns||96)-2));continue}const r=E(s,u);if(r&&0===r.prompt.length)d.info("用法: /<skill-name> [/<skill-name> ...] 你的请求");else{m||(t.updateTitle(e.id,g(s)),m=!0),f=!0,d.debug(`chat.repl send start · chars=${String(s.length)}`);for await(const t of e.send(s))await c.handle(t,e);d.debug("chat.repl send completed")}}}finally{n.close(),o&&!f&&0===t.countMessages(e.id)&&t.deleteThread(e.id)}}export default t({meta:{description:"会话式 AI 助手(多轮对话 + 自动工具调用)"},args:{message:{type:"positional",description:"起始消息",required:!1},session:{type:"string",description:"继续已有会话的 session ID"},last:{type:"boolean",description:"继续最近一个会话",default:!1},list:{type:"boolean",description:"列出已有会话",default:!1},json:{type:"boolean",description:"JSON 格式输出",default:!1},server:{type:"boolean",description:"以 JSON-RPC daemon 模式运行(stdio,供 GUI/前端接入)",default:!1},"screen-mode":{type:"string",description:`交互界面模式(${m.join("|")})`}},async run({args:e}){let{config:t}=s();const o=S({configMode:t.chat.screenMode,...void 0!==e["screen-mode"]?{cliValue:e["screen-mode"]}:{},messagePresent:void 0!==e.message,json:e.json,server:e.server,list:e.list});if(!o.ok)return d.error(o.error),void(process.exitCode=1);if(e.server)return void await U(t);if(e.list)return void await L(t,e.json);let n=resolveChatLlmReadiness(t);if(!n.configured){!0===process.stdin.isTTY&&!0===process.stderr.isTTY&&!e.json&&await _(n.provider,n.model)&&(t=s().config,n=resolveChatLlmReadiness(t))}if(!n.configured||!n.providerConfig)return d.error(n.message),void(process.exitCode=1);const i=n.providerConfig,a=n.provider,c=n.model;if(e.json&&!e.message)return d.error('--json 模式需要消息:roll chat "<message>" --json'),void(process.exitCode=1);const u=e.message?void 0:w({mode:o.mode,source:o.source,capabilities:await C({stdinIsTty:!0===process.stdin.isTTY,stdoutIsTty:!0===process.stdout.isTTY,rawModeSupported:"function"==typeof process.stdin.setRawMode,env:process.env})});if(void 0!==u&&!u.ok)return d.error(u.error),void(process.exitCode=1);u?.warning?d.warn(u.warning):u?.presentation===v.inline&&"requested"!==u.reason&&d.debug(`chat 自动选择基础 REPL:${u.reason}`);const m=await $(),{ThreadStore:k}=m,{model:E,providerOptions:T,structuredOutputProviderOptions:b,structuredOutputReasoning:N}=resolveChatLlmCalls(a,c,i.apiKey,i.baseUrl,t.runtime.thinkingLevel,t.runtime.compaction.thinkingLevel,"summarize"===t.runtime.compaction.strategy),I=new k(t.runtime.threadsDir),x=e.message?e.json?CHAT_ENGINE_SURFACES.json:CHAT_ENGINE_SURFACES.oneShot:u?.presentation===v.fullscreen?CHAT_ENGINE_SURFACES.ink:CHAT_ENGINE_SURFACES.basicRepl,A=j();let G,P;try{let o;if(G=createChatEngine({runtime:m,config:t,model:E,store:I,surface:x,shellEnv:A.env,...T?{providerOptions:T}:{},...b?{structuredOutputProviderOptions:b}:{},...N?{structuredOutputReasoning:N}:{}}),e.session)o=await G.resumeSession(e.session);else if(e.last){const e=I.listThreads()[0];if(!e)return d.error('暂无可继续的会话,先用 `roll chat "<message>"` 开始一个'),void(process.exitCode=1);o=await G.resumeSession(e.id)}else o=await G.createSession(e.message?{title:g(e.message)}:{});if(P=o,e.json&&e.message){const t=await runJsonTurn(o,e.message);return R(t),void("completed"!==t.status&&(process.exitCode=1))}if(u?.presentation!==v.fullscreen&&d.info(`会话 ${o.id}`),t.runtime.compaction.enabled&&void 0===o.getContextWindow()&&d.warn(`未知模型 "${c}" 的 context window,阈值自动压缩不可用。可在 roll.config.yaml 设置 runtime.context-window`),e.message){const t=new l(p,o.getContextWindow());for await(const s of o.send(e.message))await t.handle(s,o)}else{const s=!e.session&&!e.last,n=await G.getContextSummary(),i={version:y(),model:c,agentCount:n.agentCount,skillCount:n.skillCount};let l=!1;if(u?.presentation===v.fullscreen)try{const e=new URL(`../chat/ink/run-ink-repl.${O}`,import.meta.url).href,{runInkRepl:n}=await import(e);await n(o,I,s,{model:c,banner:i,initialThinkingLevel:t.runtime.thinkingLevel,onStarted:()=>{l=!0},onThinkingChange:e=>o.setProviderOptions(r(a,c,e))})}catch(e){if(l)throw e;d.info(`会话 ${o.id}`),d.warn(`Ink TUI 不可用,回退到基础多轮模式:${e instanceof Error?e.message:String(e)}`)}l||(process.stderr.write(`${h(f(i,process.stdout.columns||80))}\n`),await runRepl(o,I,s))}}catch(e){d.error(e instanceof Error?e.message:String(e)),process.exitCode=1}finally{await(P?.close()),await(G?.dispose()),I.close(),A.dispose()}}});