task_terminal.html 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <pstm-uri>/tasks/{task_id}/terminal</pstm-uri>
  5. <pstm-template>main</pstm-template>
  6. <script>
  7. if (window.statum) {
  8. (function () {
  9. 'use strict';
  10. var connected = false;
  11. statum.addStateListener('terminal', function () {
  12. var state = statum.read('terminal');
  13. if (!connected && state && state.exists && state.session_id > 0) {
  14. if (window.initQuoteComments && !state.has_raw_log) {
  15. window.initQuoteComments({
  16. matches: function (node) {
  17. return node.closest && node.closest('#term-log') != null;
  18. },
  19. action: function () { return state.comment; }
  20. });
  21. }
  22. connect(state.session_id, !!state.has_raw_log);
  23. }
  24. });
  25. function connect(sessionId, rawMode) {
  26. connected = true;
  27. var log = document.getElementById(rawMode ? 'term-raw' : 'term-log');
  28. var source = new EventSource('/terminal-stream/' + sessionId);
  29. var lastId = 0;
  30. var lastSeq = 0;
  31. source.onmessage = function (event) {
  32. if (event.data === 'ping') return;
  33. var entry = null;
  34. try { entry = JSON.parse(event.data); } catch (e) { return; }
  35. if (!entry) return;
  36. if (rawMode) {
  37. // Raw PTY log: one unstyled <pre> of concatenated
  38. // chunks, byte-exact as the daemon produced them.
  39. if (entry.type !== 'raw' || entry.seq <= lastSeq) return;
  40. lastSeq = entry.seq;
  41. var ph = log.querySelector('.term-placeholder');
  42. if (ph) ph.remove();
  43. var near = log.scrollHeight - log.scrollTop - log.clientHeight < 48;
  44. log.appendChild(document.createTextNode(entry.text));
  45. if (near) log.scrollTop = log.scrollHeight;
  46. return;
  47. }
  48. if (entry.id <= lastId) return;
  49. lastId = entry.id;
  50. var placeholder = log.querySelector('.term-placeholder');
  51. if (placeholder) placeholder.remove();
  52. var nearBottom = log.scrollHeight - log.scrollTop - log.clientHeight < 48;
  53. appendEntry(log, entry);
  54. if (nearBottom) log.scrollTop = log.scrollHeight;
  55. };
  56. }
  57. function appendEntry(log, entry) {
  58. // Only render bash entries — llm-comment entries duplicate
  59. // the # lines already present in the bash command_text
  60. var type = entry.type || 'command';
  61. // Recoverable rejections (bash -n / max_tokens) — a
  62. // distinct non-fatal style; the agent retries on its own
  63. if (type === 'rejected') {
  64. var ph = log.querySelector('.term-placeholder');
  65. if (ph) ph.remove();
  66. if (entry.command && entry.command.length > 0) {
  67. var script = document.createElement('span');
  68. script.className = 'term-stdin';
  69. script.textContent = '$ ' + entry.command + '\n';
  70. log.appendChild(script);
  71. }
  72. var reason = document.createElement('span');
  73. reason.className = 'term-rejected';
  74. reason.textContent = '[rejected] ' + (entry.output || 'recoverable rejection') + '\n';
  75. log.appendChild(reason);
  76. log.appendChild(document.createTextNode('\n'));
  77. return;
  78. }
  79. if (type !== 'bash' && type !== 'command') return;
  80. // stdin: the LLM's response sent to bash — render per-line
  81. if (entry.command && entry.command.length > 0) {
  82. var lines = entry.command.split('\n');
  83. for (var i = 0; i < lines.length; i++) {
  84. var line = lines[i];
  85. if (line.length === 0) continue;
  86. var span = document.createElement('span');
  87. if (line.charAt(0) === '#') {
  88. span.className = 'term-think';
  89. span.textContent = line + '\n';
  90. } else {
  91. span.className = 'term-stdin';
  92. span.textContent = '$ ' + line + '\n';
  93. }
  94. log.appendChild(span);
  95. }
  96. }
  97. // piped input (saddle query)
  98. if (entry.input && entry.input.length > 0) {
  99. var inp = document.createElement('span');
  100. inp.className = 'term-stdin';
  101. inp.textContent = entry.input + '\n';
  102. log.appendChild(inp);
  103. }
  104. // stdout/stderr
  105. if (entry.output && entry.output.length > 0) {
  106. var out = document.createElement('span');
  107. out.className = (entry.exit_code !== 0 && entry.exit_code !== -1)
  108. ? 'term-stderr' : 'term-stdout';
  109. out.textContent = entry.output + '\n';
  110. log.appendChild(out);
  111. }
  112. log.appendChild(document.createTextNode('\n'));
  113. }
  114. })();
  115. }
  116. </script>
  117. </head>
  118. <body>
  119. <section>
  120. <h2>Terminal — <span stm-text="terminal.task_title">task</span></h2>
  121. <p stm-if="terminal != null && !terminal.exists" class="muted">Task not found.</p>
  122. <div stm-if="terminal != null && terminal.exists">
  123. <p><a stm-attribute.href="'/tasks/' + terminal.task_id" stm-text="'Back to task #' + terminal.task_id">task</a>
  124. · session <strong stm-text="terminal.session_id">id</strong></p>
  125. <p stm-if="terminal.sessions.length > 1">Sessions:
  126. <span stm-for-session-in="terminal.sessions">
  127. <a stm-if="session.selected"
  128. stm-attribute.href="'/tasks/' + terminal.task_id + '/terminal?session_id=' + session.id"
  129. stm-text="session.label">session</a>
  130. <a stm-if="!session.selected" class="muted"
  131. stm-attribute.href="'/tasks/' + terminal.task_id + '/terminal?session_id=' + session.id"
  132. stm-text="session.label">session</a>
  133. </span>
  134. </p>
  135. <pre class="terminal-plain terminal-raw" stm-if="terminal.has_raw_log" id="term-raw"><span class="muted term-placeholder">Connecting…</span></pre>
  136. <div class="terminal-plain" stm-if="terminal.sessions.length > 0 && !terminal.has_raw_log" id="term-log">
  137. <span class="muted term-placeholder">Connecting…</span>
  138. </div>
  139. <p stm-if="terminal.sessions.length == 0" class="muted">No sessions recorded for this task yet.</p>
  140. <dialog id="quote-comment-dialog">
  141. <h3>Comment on selected text</h3>
  142. <blockquote id="quote-text" class="term-comment-quote"></blockquote>
  143. <form stm-action="terminal.comment">
  144. <input type="hidden" name="quote">
  145. <input type="hidden" name="return_to">
  146. <textarea name="body" rows="4" placeholder="Your comment…" style="width:100%; margin:0.5em 0;"></textarea>
  147. <button type="submit" class="primary">Send comment</button>
  148. <button type="button" onclick="document.getElementById('quote-comment-dialog').close()">Cancel</button>
  149. </form>
  150. </dialog>
  151. </div>
  152. </section>
  153. </body>
  154. </html>