pinglet-cli 0.8.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -43,6 +43,7 @@ const picker_1 = require("../picker");
43
43
  const queue_1 = require("../queue");
44
44
  const claude_1 = require("../adapters/claude");
45
45
  const runtime_1 = require("../runtime");
46
+ const turn_1 = require("../turn");
46
47
  const hud_1 = require("../hud");
47
48
  function readPayload() {
48
49
  try {
@@ -73,6 +74,27 @@ function trackSpinnerActivity(state, payload) {
73
74
  return;
74
75
  state.current.visibleMs = Math.min((state.current.visibleMs ?? 0) + (apiMs - prev), runtime_1.ROTATE_MS);
75
76
  }
77
+ /**
78
+ * spinner는 시작할 때 고른 문구를 턴 끝까지 유지한다. spinnerVerbs는 모든 세션이
79
+ * 공유하므로, 어느 세션이든 턴이 진행 중이면 회전을 미뤄 spinner와 statusline을 맞춘다.
80
+ * 세션이 턴 도중 닫혀도 TURN_STALE_MS 뒤에는 기록이 풀린다.
81
+ */
82
+ function updateBusySessions(state, payload, now) {
83
+ const busy = state.busySessions ?? {};
84
+ const sessionId = payload.session_id;
85
+ if (sessionId) {
86
+ if ((0, turn_1.isTurnInProgress)(payload.transcript_path, now))
87
+ busy[sessionId] = now;
88
+ else
89
+ delete busy[sessionId];
90
+ }
91
+ for (const [id, seenAt] of Object.entries(busy)) {
92
+ if (now - seenAt > turn_1.TURN_STALE_MS)
93
+ delete busy[id];
94
+ }
95
+ state.busySessions = Object.keys(busy).length > 0 ? busy : undefined;
96
+ return state.busySessions !== undefined;
97
+ }
76
98
  /**
77
99
  * 이전 armed 메시지의 노출을 정산하고 다음 메시지를 armed한다.
78
100
  * QUALIFIED_MS 이상 spinner가 돌았으면 유효 노출(visibleMs 포함)로 기록한다.
@@ -141,8 +163,11 @@ function runStatusline() {
141
163
  const state = (0, cache_1.loadState)();
142
164
  const now = Date.now();
143
165
  trackSpinnerActivity(state, payload);
144
- const rotateDue = !state.current || now - state.current.shownAt >= runtime_1.ROTATE_MS ||
145
- !(0, cache_1.loadFeedMessages)().some((message) => message.id === state.current?.messageId);
166
+ const turnBusy = updateBusySessions(state, payload, now);
167
+ // 피드에서 빠진(삭제·만료) 항목은 턴 중이라도 바로 내린다.
168
+ const rotateDue = !state.current ||
169
+ !(0, cache_1.loadFeedMessages)().some((message) => message.id === state.current?.messageId) ||
170
+ (!turnBusy && now - state.current.shownAt >= runtime_1.ROTATE_MS);
146
171
  // 300ms마다 호출되므로 저장/maintenance는 1초 스로틀. 회전 시점에는 항상 저장.
147
172
  const saveDue = rotateDue ||
148
173
  state.lastTickAt === undefined ||
package/dist/turn.js ADDED
@@ -0,0 +1,100 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.TURN_STALE_MS = void 0;
37
+ exports.isTurnInProgress = isTurnInProgress;
38
+ const fs = __importStar(require("fs"));
39
+ /** transcript 끝에서 읽는 양 — 마지막 몇 줄만 보면 된다. 큰 tool_result 한 줄보다는 넉넉하게. */
40
+ const TAIL_BYTES = 256 * 1024;
41
+ /**
42
+ * transcript가 이만큼 조용하면 진행 중으로 보지 않는다. 비정상 종료·로컬 명령처럼
43
+ * 응답이 기록되지 않는 경우 회전이 영원히 멈추지 않게 하는 상한.
44
+ */
45
+ exports.TURN_STALE_MS = 10 * 60_000;
46
+ function isInterrupt(entry) {
47
+ const content = entry.message?.content;
48
+ const texts = typeof content === "string" ? [content]
49
+ : Array.isArray(content) ? content.map((part) => part?.text).filter((t) => typeof t === "string")
50
+ : [];
51
+ return texts.some((text) => text.startsWith("[Request interrupted"));
52
+ }
53
+ /**
54
+ * 이 세션의 턴이 진행 중인지(= spinner가 돌고 있는지) transcript 꼬리로 판단한다.
55
+ * Claude Code는 spinner가 시작될 때 고른 문구를 턴이 끝날 때까지 유지하므로,
56
+ * 진행 중에 spinnerVerbs를 바꾸면 spinner와 statusline이 서로 다른 항목을 보여준다.
57
+ * 판단할 수 없으면 false — 예전처럼 회전한다.
58
+ */
59
+ function isTurnInProgress(transcriptPath, now) {
60
+ if (!transcriptPath)
61
+ return false;
62
+ let fd;
63
+ try {
64
+ const stat = fs.statSync(transcriptPath);
65
+ if (now - stat.mtimeMs > exports.TURN_STALE_MS)
66
+ return false;
67
+ const length = Math.min(stat.size, TAIL_BYTES);
68
+ const buffer = Buffer.alloc(length);
69
+ fd = fs.openSync(transcriptPath, "r");
70
+ fs.readSync(fd, buffer, 0, length, stat.size - length);
71
+ const lines = buffer.toString("utf8").split("\n");
72
+ if (length < stat.size)
73
+ lines.shift(); // 잘린 첫 줄
74
+ for (let i = lines.length - 1; i >= 0; i--) {
75
+ let entry;
76
+ try {
77
+ entry = JSON.parse(lines[i]);
78
+ }
79
+ catch {
80
+ continue;
81
+ }
82
+ if (entry.type === "system" && entry.subtype === "turn_duration")
83
+ return false;
84
+ if (entry.type === "assistant") {
85
+ const stop = entry.message?.stop_reason;
86
+ return stop === "tool_use" || stop == null;
87
+ }
88
+ if (entry.type === "user")
89
+ return !isInterrupt(entry);
90
+ }
91
+ return false;
92
+ }
93
+ catch {
94
+ return false;
95
+ }
96
+ finally {
97
+ if (fd !== undefined)
98
+ fs.closeSync(fd);
99
+ }
100
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinglet-cli",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "Short messages from other developers shown in your Claude Code spinner while the AI thinks — zero tokens, never reads your code. Claude Code / Codex integration",
5
5
  "license": "MIT",
6
6
  "repository": {