ispbills-icli 8.4.0 → 8.4.2

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ispbills-icli",
3
- "version": "8.4.0",
3
+ "version": "8.4.2",
4
4
  "description": "iCli — IspBills AI network engineer in your terminal",
5
5
  "keywords": [
6
6
  "ispbills",
package/src/client.js CHANGED
@@ -78,7 +78,9 @@ export function parsePartialPlan(raw) {
78
78
  */
79
79
  export async function streamChat(cfg, messages, options = {}) {
80
80
  const { onEvent = () => {}, signal, context = {} } = options;
81
- const body = JSON.stringify({ messages, context });
81
+ // Always identify ourselves as the icli client so the backend can route
82
+ // away from type=connect (which requires an interactive browser terminal).
83
+ const body = JSON.stringify({ messages, context: { ...context, client: 'icli' } });
82
84
 
83
85
  const doFetch = (token) =>
84
86
  fetch(`${cfg.url}/api/v1/ai/chat/stream`, {
@@ -153,6 +155,10 @@ export async function streamChat(cfg, messages, options = {}) {
153
155
 
154
156
  const reply = finalResult?.reply ?? {};
155
157
  const meta = finalResult?._meta ?? {};
156
- const text = reply.text || answerText || '';
158
+ // Prefer whichever is longer: the final parsed reply.text or the text we
159
+ // streamed live. Guards against any server-side cap silently shortening the
160
+ // committed answer below what the operator already saw stream by.
161
+ const finalText = reply.text || '';
162
+ const text = finalText.length >= answerText.length ? finalText : answerText;
157
163
  return { reply, text, meta };
158
164
  }
package/src/tui/app.js CHANGED
@@ -96,9 +96,11 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion })
96
96
  }, []);
97
97
 
98
98
  const runTurn = useCallback(async (question, opts = {}) => {
99
- const { depth = 0, archive = null, label = '' } = opts;
100
- push({ type: 'user', text: question });
101
- log(`\n${glyphs.prompt} ${question}\n`);
99
+ const { depth = 0, archive = null, label = '', synthetic = false } = opts;
100
+ if (!synthetic) {
101
+ push({ type: 'user', text: question });
102
+ log(`\n${glyphs.prompt} ${question}\n`);
103
+ }
102
104
  convo.current.push({ role: 'user', content: question });
103
105
  setBusy(true);
104
106
  setHint('');
@@ -136,7 +138,19 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion })
136
138
  // so it stays in history instead of vanishing with the live region.
137
139
  if (s.status.length) push({ type: 'activity', status: s.status });
138
140
  if (type === 'plan') { push({ type: 'plan', reply }); log('[plan] ' + (reply.summary || '')); }
139
- else if (type === 'connect') { push({ type: 'connect', reply }); log('[connect] ' + (reply.device?.kind || '')); }
141
+ else if (type === 'connect') {
142
+ push({ type: 'connect', reply }); log('[connect] ' + (reply.device?.kind || ''));
143
+ // The icli has no interactive terminal — send a synthetic session-event
144
+ // so the backend continues to the plan/answer (rule 4). Depth-guard to
145
+ // prevent a loop if the backend keeps emitting connect (shouldn't happen).
146
+ if (depth < 2) {
147
+ const dev = reply.device || {};
148
+ const goal = convo.current.filter((m) => m.role === 'user').slice(-1)[0]?.content || '';
149
+ const sessionEvent = `[session-event] Terminal session opened on ${dev.kind || 'device'} id=${dev.id || '?'} proto=${dev.proto || 'ssh'}. vendor=auto. OPERATOR GOAL: ${goal}`;
150
+ setLive(null); setBusy(false); abortRef.current = null;
151
+ return runTurn(sessionEvent, { depth: depth + 1, synthetic: true });
152
+ }
153
+ }
140
154
  else if (type === 'prompt' || type === 'choice') { push({ type: 'assistant', text: text || reply.question || '' }); log(text || reply.question || ''); }
141
155
  else { push({ type: 'assistant', text: text || '(no response)' }); log(text || ''); }
142
156
  if (text) convo.current.push({ role: 'assistant', content: text });