Chat updates
This commit is contained in:
+255
-84
@@ -625,6 +625,55 @@ async function bareChatReadProcSnapshot(ctx) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown> | null} snap
|
||||
* @param {{
|
||||
* swarmPeers: { current: number | null },
|
||||
* muxRx: { current: number | null },
|
||||
* muxTx: { current: number | null },
|
||||
* wireRxTotal: { current: number | null },
|
||||
* dropRate: { current: number | null },
|
||||
* dropVerify: { current: number | null }
|
||||
* }} out
|
||||
*/
|
||||
function bareChatApplyProcSnap(snap, out) {
|
||||
if (!snap || typeof snap !== 'object') return
|
||||
const sp = snap.swarmPeers
|
||||
out.swarmPeers.current =
|
||||
typeof sp === 'number' && Number.isFinite(sp) ? sp : null
|
||||
const met = snap.metrics
|
||||
if (met && typeof met === 'object') {
|
||||
const m = /** @type {{ rxEvent?: number, txEvent?: number, droppedRate?: number, droppedVerify?: number }} */ (
|
||||
met
|
||||
)
|
||||
out.muxRx.current =
|
||||
typeof m.rxEvent === 'number' && Number.isFinite(m.rxEvent)
|
||||
? m.rxEvent
|
||||
: null
|
||||
out.muxTx.current =
|
||||
typeof m.txEvent === 'number' && Number.isFinite(m.txEvent)
|
||||
? m.txEvent
|
||||
: null
|
||||
out.dropRate.current =
|
||||
typeof m.droppedRate === 'number' && Number.isFinite(m.droppedRate)
|
||||
? m.droppedRate
|
||||
: null
|
||||
out.dropVerify.current =
|
||||
typeof m.droppedVerify === 'number' &&
|
||||
Number.isFinite(m.droppedVerify)
|
||||
? m.droppedVerify
|
||||
: null
|
||||
} else {
|
||||
out.muxRx.current = null
|
||||
out.muxTx.current = null
|
||||
out.dropRate.current = null
|
||||
out.dropVerify.current = null
|
||||
}
|
||||
const wire = snap.protomuxChatRxTotal
|
||||
out.wireRxTotal.current =
|
||||
typeof wire === 'number' && Number.isFinite(wire) ? wire : null
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {string} argv0
|
||||
@@ -646,6 +695,13 @@ async function bareOsRunChatTui(ctx, argv0) {
|
||||
? /** @type {Record<string, string>} */ (ctx.env)
|
||||
: {}
|
||||
|
||||
let tickMs = 1000
|
||||
const tickRaw = envEarly.BARE_CHAT_STATUS_MS
|
||||
if (tickRaw != null && String(tickRaw) !== '') {
|
||||
const n = Number.parseInt(String(tickRaw), 10)
|
||||
if (Number.isFinite(n) && n >= 0) tickMs = Math.min(Math.max(0, n), 3_600_000)
|
||||
}
|
||||
|
||||
/** @type {'main'|'help'} */
|
||||
let mode = 'main'
|
||||
|
||||
@@ -659,21 +715,28 @@ async function bareOsRunChatTui(ctx, argv0) {
|
||||
let inputBuf = ''
|
||||
let inputCursor = 0
|
||||
|
||||
/** @type {number|null} */
|
||||
let swarmPeers = null
|
||||
/** @type {number|null} */
|
||||
let muxRx = null
|
||||
const metricRef = {
|
||||
swarmPeers: /** @type {{ current: number | null }} */ ({ current: null }),
|
||||
muxRx: /** @type {{ current: number | null }} */ ({ current: null }),
|
||||
muxTx: /** @type {{ current: number | null }} */ ({ current: null }),
|
||||
wireRxTotal: /** @type {{ current: number | null }} */ ({ current: null }),
|
||||
dropRate: /** @type {{ current: number | null }} */ ({ current: null }),
|
||||
dropVerify: /** @type {{ current: number | null }} */ ({ current: null })
|
||||
}
|
||||
|
||||
async function refreshProcStrip() {
|
||||
const snap = await bareChatReadProcSnapshot(ctx)
|
||||
if (!snap || typeof snap !== 'object') return
|
||||
const sp = snap.swarmPeers
|
||||
if (typeof sp === 'number' && Number.isFinite(sp)) swarmPeers = sp
|
||||
const met = snap.metrics
|
||||
if (met && typeof met === 'object') {
|
||||
const rx = /** @type {{ rxEvent?: number }} */ (met).rxEvent
|
||||
if (typeof rx === 'number') muxRx = rx
|
||||
let snap = null
|
||||
if (typeof ctx.bareOsChatProcSnapshot === 'function') {
|
||||
try {
|
||||
snap = ctx.bareOsChatProcSnapshot()
|
||||
} catch {
|
||||
snap = null
|
||||
}
|
||||
}
|
||||
if (!snap || typeof snap !== 'object') {
|
||||
snap = await bareChatReadProcSnapshot(ctx)
|
||||
}
|
||||
bareChatApplyProcSnap(snap, metricRef)
|
||||
}
|
||||
|
||||
function clampScroll(viewH) {
|
||||
@@ -694,37 +757,6 @@ async function bareOsRunChatTui(ctx, argv0) {
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {(() => void) | null} */
|
||||
let unsub = null
|
||||
if (typeof ctx.bareOsChatSubscribe === 'function') {
|
||||
unsub = ctx.bareOsChatSubscribe((ev) => {
|
||||
appendEvent(
|
||||
/** @type {Record<string, unknown>} */ (
|
||||
ev && typeof ev === 'object' ? ev : {}
|
||||
)
|
||||
)
|
||||
draw()
|
||||
})
|
||||
}
|
||||
|
||||
if (typeof ctx.bareOsChatHistory === 'function') {
|
||||
try {
|
||||
const hist = ctx.bareOsChatHistory(BARE_CHAT_MAX_TRANSCRIPT)
|
||||
if (Array.isArray(hist)) {
|
||||
for (const h of hist) {
|
||||
if (h && typeof h === 'object') {
|
||||
bareChatPushTranscript(transcript, bareChatFmtEventLine(
|
||||
/** @type {Record<string, unknown>} */ (h)
|
||||
))
|
||||
}
|
||||
}
|
||||
stickToBottom = true
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
function termDims() {
|
||||
const cols = /** @type {{ columns?: number }} */ (stdout).columns ||
|
||||
parseInt(envEarly.COLUMNS || '80', 10) ||
|
||||
@@ -735,6 +767,9 @@ async function bareOsRunChatTui(ctx, argv0) {
|
||||
return { cols: Math.max(40, cols), rows: Math.max(10, rows) }
|
||||
}
|
||||
|
||||
let paintBusy = false
|
||||
let paintAgain = false
|
||||
|
||||
function draw() {
|
||||
const { cols, rows } = termDims()
|
||||
const headerRows = 3
|
||||
@@ -752,8 +787,9 @@ async function bareOsRunChatTui(ctx, argv0) {
|
||||
'\r\n\r\n' +
|
||||
'Send with Enter. Scroll transcript with \u2191/\u2193 or PgUp/PgDn.\r\n' +
|
||||
'Input: Backspace/Delete, \u2190/\u2192, Home/End, ^A/^E line start/end, ^U kill to start, ^K kill to end.\r\n' +
|
||||
'^R refresh strip ^L redraw ^Q / ^X / Ctrl+C exit\r\n' +
|
||||
'`chat send TEXT` / `chat who` / `chat history` remain for scripts.\r\n\r\n' +
|
||||
'^R refresh status ^L redraw ^Q / ^X / Ctrl+C exit\r\n' +
|
||||
'`chat send TEXT` / `chat who` / `chat history` remain for scripts.\r\n' +
|
||||
'Env: BARE_CHAT_STATUS_MS — status refresh interval in ms (default 1000; 0 disables timer).\r\n\r\n' +
|
||||
'Press any key.\r\n'
|
||||
bareEditWrite(ctx, stdout, out + '\x1b[?25h')
|
||||
return
|
||||
@@ -766,35 +802,59 @@ async function bareOsRunChatTui(ctx, argv0) {
|
||||
const roomStr = Array.isArray(rooms) && rooms.length ? rooms.join(', ') : 'general'
|
||||
|
||||
const peerStr =
|
||||
swarmPeers != null ? String(swarmPeers) : '?'
|
||||
const rxStr = muxRx != null ? String(muxRx) : '?'
|
||||
metricRef.swarmPeers.current != null
|
||||
? String(metricRef.swarmPeers.current)
|
||||
: '?'
|
||||
const rxStr =
|
||||
metricRef.muxRx.current != null ? String(metricRef.muxRx.current) : '?'
|
||||
const txStr =
|
||||
metricRef.muxTx.current != null ? String(metricRef.muxTx.current) : '?'
|
||||
const wireStr =
|
||||
metricRef.wireRxTotal.current != null
|
||||
? String(metricRef.wireRxTotal.current)
|
||||
: '?'
|
||||
|
||||
/** @type {string[]} */
|
||||
const titleParts = [
|
||||
argv0 || 'chat',
|
||||
roomStr,
|
||||
'peers ' + peerStr,
|
||||
'rx ' + rxStr,
|
||||
'tx ' + txStr,
|
||||
'wire ' + wireStr
|
||||
]
|
||||
const dr = metricRef.dropRate.current ?? 0
|
||||
const dv = metricRef.dropVerify.current ?? 0
|
||||
if (dr > 0 || dv > 0) {
|
||||
titleParts.push('drops r' + dr + '/v' + dv)
|
||||
}
|
||||
|
||||
const title =
|
||||
bareEditSgr('status', useColor) +
|
||||
bareChatTruncateVis(
|
||||
' ' +
|
||||
(argv0 || 'chat') +
|
||||
' · ' +
|
||||
roomStr +
|
||||
' · peers ' +
|
||||
peerStr +
|
||||
' · rx ' +
|
||||
rxStr +
|
||||
' ',
|
||||
' \u250c ' + titleParts.join(' \u00b7 ') + ' ',
|
||||
cols
|
||||
) +
|
||||
EDIT_ANSI_RESET
|
||||
|
||||
out += bareEditCup(1, 1) + '\x1b[K' + title
|
||||
const nowClock = bareChatFmtClock(Date.now())
|
||||
const hint =
|
||||
bareEditSgr('dim', useColor) +
|
||||
bareChatTruncateVis(
|
||||
'bare-os-chat-v1 · ? help · ^Q quit · ^R refresh strip',
|
||||
nowClock +
|
||||
' bare-os-chat-v1 ? help ^Q quit ^R refresh' +
|
||||
(tickMs > 0 ? ' tick ' + tickMs + 'ms' : ' tick off'),
|
||||
cols
|
||||
) +
|
||||
EDIT_ANSI_RESET
|
||||
out += bareEditCup(1, 1) + '\x1b[K' + title
|
||||
out += bareEditCup(2, 1) + '\x1b[K' + hint
|
||||
out += bareEditCup(3, 1) + '\x1b[K' + bareEditSgr('dim', useColor) + '\u2500'.repeat(Math.min(cols, 120)) + EDIT_ANSI_RESET
|
||||
out +=
|
||||
bareEditCup(3, 1) +
|
||||
'\x1b[K' +
|
||||
bareEditSgr('dim', useColor) +
|
||||
'\u2500'.repeat(Math.min(cols, 120)) +
|
||||
EDIT_ANSI_RESET
|
||||
|
||||
for (let i = 0; i < msgH; i++) {
|
||||
const idx = scrollTop + i
|
||||
@@ -823,10 +883,19 @@ async function bareOsRunChatTui(ctx, argv0) {
|
||||
EDIT_ANSI_RESET
|
||||
|
||||
const hintRow = sepRow + 1
|
||||
const linesBelow = Math.max(0, transcript.length - scrollTop - msgH)
|
||||
let scrollHint = ''
|
||||
if (!stickToBottom && (scrollTop > 0 || linesBelow > 0)) {
|
||||
const parts = []
|
||||
if (scrollTop > 0) parts.push('\u2191 ' + scrollTop + ' older')
|
||||
if (linesBelow > 0) parts.push('\u2193 ' + linesBelow + ' newer')
|
||||
scrollHint = parts.join(' ')
|
||||
}
|
||||
const hint2 =
|
||||
bareEditSgr('dim', useColor) +
|
||||
bareChatTruncateVis(
|
||||
'Enter send \u2191\u2193 transcript Backspace / ^A ^E ^U ^K',
|
||||
(scrollHint ? scrollHint + ' ' : '') +
|
||||
'Enter send \u2191\u2193 transcript Backspace / ^A ^E ^U ^K',
|
||||
cols
|
||||
) +
|
||||
EDIT_ANSI_RESET
|
||||
@@ -866,11 +935,68 @@ async function bareOsRunChatTui(ctx, argv0) {
|
||||
bareEditWrite(ctx, stdout, out + '\x1b[?25h')
|
||||
}
|
||||
|
||||
function paint() {
|
||||
if (paintBusy) {
|
||||
paintAgain = true
|
||||
return
|
||||
}
|
||||
paintBusy = true
|
||||
try {
|
||||
do {
|
||||
paintAgain = false
|
||||
draw()
|
||||
} while (paintAgain)
|
||||
} finally {
|
||||
paintBusy = false
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {(() => void) | null} */
|
||||
let unsub = null
|
||||
if (typeof ctx.bareOsChatSubscribe === 'function') {
|
||||
unsub = ctx.bareOsChatSubscribe((ev) => {
|
||||
appendEvent(
|
||||
/** @type {Record<string, unknown>} */ (
|
||||
ev && typeof ev === 'object' ? ev : {}
|
||||
)
|
||||
)
|
||||
void refreshProcStrip().then(() => paint())
|
||||
})
|
||||
}
|
||||
|
||||
if (typeof ctx.bareOsChatHistory === 'function') {
|
||||
try {
|
||||
const hist = ctx.bareOsChatHistory(BARE_CHAT_MAX_TRANSCRIPT)
|
||||
if (Array.isArray(hist)) {
|
||||
for (const h of hist) {
|
||||
if (h && typeof h === 'object') {
|
||||
bareChatPushTranscript(
|
||||
transcript,
|
||||
bareChatFmtEventLine(
|
||||
/** @type {Record<string, unknown>} */ (h)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
stickToBottom = true
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
const reader = bareEditCreateStdinReader(stdin)
|
||||
let suspended = false
|
||||
let useAltScreen = false
|
||||
let useBracketPaste = false
|
||||
|
||||
/** @type {ReturnType<typeof setInterval> | null} */
|
||||
let tickTimer = null
|
||||
|
||||
function onResize() {
|
||||
void refreshProcStrip().then(() => paint())
|
||||
}
|
||||
|
||||
await refreshProcStrip()
|
||||
|
||||
try {
|
||||
@@ -897,7 +1023,28 @@ async function bareOsRunChatTui(ctx, argv0) {
|
||||
useBracketPaste = true
|
||||
}
|
||||
|
||||
draw()
|
||||
if (typeof stdout.on === 'function') {
|
||||
try {
|
||||
stdout.on('resize', onResize)
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
try {
|
||||
if (typeof process !== 'undefined' && typeof process.on === 'function') {
|
||||
process.on('SIGWINCH', onResize)
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
|
||||
if (tickMs > 0 && typeof setInterval === 'function') {
|
||||
tickTimer = setInterval(() => {
|
||||
void refreshProcStrip().then(() => paint())
|
||||
}, tickMs)
|
||||
}
|
||||
|
||||
paint()
|
||||
|
||||
for (;;) {
|
||||
const ev = await bareEditReadKey(reader)
|
||||
@@ -905,7 +1052,7 @@ async function bareOsRunChatTui(ctx, argv0) {
|
||||
|
||||
if (mode === 'help') {
|
||||
mode = 'main'
|
||||
draw()
|
||||
paint()
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -918,7 +1065,7 @@ async function bareOsRunChatTui(ctx, argv0) {
|
||||
inputBuf.slice(0, inputCursor - 1) + inputBuf.slice(inputCursor)
|
||||
inputCursor--
|
||||
}
|
||||
draw()
|
||||
paint()
|
||||
continue
|
||||
}
|
||||
if (ev.code === 'delete') {
|
||||
@@ -926,41 +1073,41 @@ async function bareOsRunChatTui(ctx, argv0) {
|
||||
inputBuf =
|
||||
inputBuf.slice(0, inputCursor) + inputBuf.slice(inputCursor + 1)
|
||||
}
|
||||
draw()
|
||||
paint()
|
||||
continue
|
||||
}
|
||||
const code = typeof ev.code === 'number' ? ev.code : 0
|
||||
if (code === 3) break
|
||||
if (code === 12) {
|
||||
draw()
|
||||
paint()
|
||||
continue
|
||||
}
|
||||
if (code === 17 || code === 24) break
|
||||
if (code === 18) {
|
||||
await refreshProcStrip()
|
||||
draw()
|
||||
paint()
|
||||
continue
|
||||
}
|
||||
/** Readline-style shortcuts (ASCII control chars). */
|
||||
if (code === 1) {
|
||||
inputCursor = 0
|
||||
draw()
|
||||
paint()
|
||||
continue
|
||||
}
|
||||
if (code === 5) {
|
||||
inputCursor = inputBuf.length
|
||||
draw()
|
||||
paint()
|
||||
continue
|
||||
}
|
||||
if (code === 21) {
|
||||
inputBuf = inputBuf.slice(inputCursor)
|
||||
inputCursor = 0
|
||||
draw()
|
||||
paint()
|
||||
continue
|
||||
}
|
||||
if (code === 11) {
|
||||
inputBuf = inputBuf.slice(0, inputCursor)
|
||||
draw()
|
||||
paint()
|
||||
continue
|
||||
}
|
||||
if (code === 8 || code === 127) {
|
||||
@@ -969,7 +1116,7 @@ async function bareOsRunChatTui(ctx, argv0) {
|
||||
inputBuf.slice(0, inputCursor - 1) + inputBuf.slice(inputCursor)
|
||||
inputCursor--
|
||||
}
|
||||
draw()
|
||||
paint()
|
||||
continue
|
||||
}
|
||||
continue
|
||||
@@ -980,7 +1127,7 @@ async function bareOsRunChatTui(ctx, argv0) {
|
||||
if (k === 'up') {
|
||||
stickToBottom = false
|
||||
if (scrollTop > 0) scrollTop--
|
||||
draw()
|
||||
paint()
|
||||
continue
|
||||
}
|
||||
if (k === 'down') {
|
||||
@@ -989,27 +1136,27 @@ async function bareOsRunChatTui(ctx, argv0) {
|
||||
const maxTop = Math.max(0, transcript.length - msgH)
|
||||
if (scrollTop < maxTop) scrollTop++
|
||||
if (scrollTop >= maxTop) stickToBottom = true
|
||||
draw()
|
||||
paint()
|
||||
continue
|
||||
}
|
||||
if (k === 'left') {
|
||||
if (inputCursor > 0) inputCursor--
|
||||
draw()
|
||||
paint()
|
||||
continue
|
||||
}
|
||||
if (k === 'right') {
|
||||
if (inputCursor < inputBuf.length) inputCursor++
|
||||
draw()
|
||||
paint()
|
||||
continue
|
||||
}
|
||||
if (k === 'home') {
|
||||
inputCursor = 0
|
||||
draw()
|
||||
paint()
|
||||
continue
|
||||
}
|
||||
if (k === 'end') {
|
||||
inputCursor = inputBuf.length
|
||||
draw()
|
||||
paint()
|
||||
continue
|
||||
}
|
||||
if (k === 'pageup') {
|
||||
@@ -1017,7 +1164,7 @@ async function bareOsRunChatTui(ctx, argv0) {
|
||||
const { rows: rr } = termDims()
|
||||
const msgH = Math.max(1, rr - 6)
|
||||
scrollTop = Math.max(0, scrollTop - msgH)
|
||||
draw()
|
||||
paint()
|
||||
continue
|
||||
}
|
||||
if (k === 'pagedown') {
|
||||
@@ -1026,7 +1173,7 @@ async function bareOsRunChatTui(ctx, argv0) {
|
||||
const maxTop = Math.max(0, transcript.length - msgH)
|
||||
scrollTop = Math.min(maxTop, scrollTop + msgH)
|
||||
if (scrollTop >= maxTop) stickToBottom = true
|
||||
draw()
|
||||
paint()
|
||||
continue
|
||||
}
|
||||
continue
|
||||
@@ -1035,7 +1182,7 @@ async function bareOsRunChatTui(ctx, argv0) {
|
||||
if (ev.type === 'key' && ev.ch) {
|
||||
if (ev.ch === '?') {
|
||||
mode = 'help'
|
||||
draw()
|
||||
paint()
|
||||
continue
|
||||
}
|
||||
const ch = ev.ch
|
||||
@@ -1048,7 +1195,7 @@ async function bareOsRunChatTui(ctx, argv0) {
|
||||
}
|
||||
stickToBottom = true
|
||||
await refreshProcStrip()
|
||||
draw()
|
||||
paint()
|
||||
continue
|
||||
}
|
||||
if (inputBuf.length < BARE_CHAT_INPUT_MAX) {
|
||||
@@ -1056,7 +1203,7 @@ async function bareOsRunChatTui(ctx, argv0) {
|
||||
inputBuf.slice(0, inputCursor) + ch + inputBuf.slice(inputCursor)
|
||||
inputCursor += ch.length
|
||||
}
|
||||
draw()
|
||||
paint()
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -1067,18 +1214,42 @@ async function bareOsRunChatTui(ctx, argv0) {
|
||||
if (first) {
|
||||
const room = Math.max(
|
||||
0,
|
||||
BARE_CHAT_INPUT_MAX - inputBuf.length + (inputBuf.length - inputCursor)
|
||||
BARE_CHAT_INPUT_MAX -
|
||||
inputBuf.length +
|
||||
(inputBuf.length - inputCursor)
|
||||
)
|
||||
const chunk = first.slice(0, room)
|
||||
inputBuf =
|
||||
inputBuf.slice(0, inputCursor) + chunk + inputBuf.slice(inputCursor)
|
||||
inputCursor += chunk.length
|
||||
}
|
||||
draw()
|
||||
paint()
|
||||
continue
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (tickTimer != null && typeof clearInterval === 'function') {
|
||||
try {
|
||||
clearInterval(tickTimer)
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
tickTimer = null
|
||||
}
|
||||
if (typeof stdout.removeListener === 'function') {
|
||||
try {
|
||||
stdout.removeListener('resize', onResize)
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
try {
|
||||
if (typeof process !== 'undefined' && typeof process.off === 'function') {
|
||||
process.off('SIGWINCH', onResize)
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
try {
|
||||
if (useBracketPaste) bareEditWrite(ctx, stdout, '\x1b[?2004l')
|
||||
if (useAltScreen) bareEditWrite(ctx, stdout, '\x1b[?1049l')
|
||||
|
||||
Reference in New Issue
Block a user